Merge branch 'master' into patch-1

This commit is contained in:
Jim LAURIE 2018-06-12 13:40:29 +02:00 committed by GitHub
commit 54569b164e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1482 additions and 576 deletions

View File

@ -7,6 +7,7 @@ language: node_js
node_js:
- "9"
- "10"
before_install:
- export CHROME_BIN=chromium-browser

View File

@ -1,6 +1,7 @@
{
"private": true,
"version": "3.0.0-alpha.12.3",
"dependencies": {},
"devDependencies": {
"assert": "~1.3.0",
"axios": "^0.18.0",

View File

@ -16,6 +16,7 @@ import messages from './messages.json';
function LeftMenuLinkContainer({ layout, plugins }) {
const pluginsObject = plugins.toJS();
// Generate the list of sections
const pluginsSections = Object.keys(pluginsObject).reduce((acc, current) => {
pluginsObject[current].leftMenuSections.forEach((section = {}) => {
@ -38,7 +39,7 @@ function LeftMenuLinkContainer({ layout, plugins }) {
return acc;
}, {});
const linkSections = Object.keys(pluginsSections).map((current, j) => {
const contentTypesToShow = get(layout, 'layout.contentTypesToShow');
const contentTypes = contentTypesToShow
@ -68,7 +69,7 @@ function LeftMenuLinkContainer({ layout, plugins }) {
// Check if the plugins list is empty or not and display plugins by name
const pluginsLinks = !isEmpty(pluginsObject) ? (
map(sortBy(pluginsObject, 'name'), plugin => {
if (plugin.id !== 'email' && plugin.id !== 'content-manager') {
if (plugin.id !== 'email' && plugin.id !== 'content-manager' && plugin.id !== 'settings-manager') {
return (
<LeftMenuLink
key={get(plugin, 'id')}
@ -85,6 +86,8 @@ function LeftMenuLinkContainer({ layout, plugins }) {
</li>
);
const hasSettingsManager = get(pluginsObject, 'settings-manager', null);
return (
<div className={styles.leftMenuLinkContainer}>
{linkSections}
@ -105,11 +108,13 @@ function LeftMenuLinkContainer({ layout, plugins }) {
label={messages.installNewPlugin.id}
destination="/install-plugin"
/>
<LeftMenuLink
icon="gear"
label={messages.configuration.id}
destination="/configuration"
/>
{hasSettingsManager && (
<LeftMenuLink
icon="gear"
label={messages.configuration.id}
destination="/plugins/settings-manager"
/>
)}
</ul>
</div>
</div>

View File

@ -20,11 +20,14 @@ const localStorageKey = 'strapi-admin-language';
// Detect user language.
const userLanguage = window.localStorage.getItem(localStorageKey) || window.navigator.language || window.navigator.userLanguage;
// Split user language in a correct format.
const userLanguageShort = get(split(userLanguage, '-'), '0');
let foundLanguage = includes(languages, userLanguage) && userLanguage;
if (!foundLanguage) {
// Split user language in a correct format.
const userLanguageShort = get(split(userLanguage, '-'), '0');
// Check that the language is included in the admin configuration.
const foundLanguage = includes(languages, userLanguageShort) && userLanguageShort;
// Check that the language is included in the admin configuration.
foundLanguage = includes(languages, userLanguageShort) && userLanguageShort;
}
const initialState = fromJS({
locale: foundLanguage || first(languages) || 'en',

View File

@ -21,3 +21,7 @@ button {
margin-right: -2rem;
}
}
form .row {
text-align: left;
}

View File

@ -918,6 +918,13 @@ module.exports = function(strapi) {
value: `%${value}%`
};
break;
case '_in':
result.key = `where.${key}`;
result.value = {
symbol: 'IN',
value,
};
break;
default:
return undefined;
}

View File

@ -179,7 +179,7 @@ module.exports = {
);
});
} else if (_.get(this._attributes, `${current}.isVirtual`) !== true) {
if (typeof params.values[current] === 'object') {
if (params.values[current] && typeof params.values[current] === 'object') {
acc[current] = _.get(params.values[current], this.primaryKey);
} else {
acc[current] = params.values[current];

View File

@ -12,7 +12,6 @@ const { execSync } = require('child_process');
const _ = require('lodash');
const fs = require('fs-extra');
const npm = require('enpeem');
const getInstalledPath = require('get-installed-path');
// Logger.
const logger = require('strapi-utils').logger;
@ -39,16 +38,17 @@ module.exports = (scope, cb) => {
const dependencies = _.get(packageJSON, 'dependencies');
const strapiDependencies = Object.keys(dependencies).filter(key => key.indexOf('strapi') !== -1);
const othersDependencies = Object.keys(dependencies).filter(key => key.indexOf('strapi') === -1);
const globalRootPath = execSync('npm root -g');
// Verify if the dependencies are available into the global
_.forEach(strapiDependencies, (key) => {
try {
const isInstalled = getInstalledPath.sync(key);
fs.accessSync(path.resolve(_.trim(globalRootPath.toString()), key), fs.constants.R_OK | fs.constants.F_OK);
availableDependencies.push({
key,
global: true,
path: isInstalled
path: path.resolve(_.trim(globalRootPath.toString()), key)
});
} catch (e) {
othersDependencies.push(key);

View File

@ -15,7 +15,6 @@
"dependencies": {
"enpeem": "^2.2.0",
"fs-extra": "^4.0.0",
"get-installed-path": "^3.0.1",
"inquirer": "^4.0.2",
"lodash": "^4.17.4",
"strapi-utils": "3.0.0-alpha.12.3",

View File

@ -144,6 +144,7 @@ InputSelectWithErrors.defaultProps = {
labelStyle: {},
onBlur: false,
onFocus: () => {},
selectOptions: [],
style: {},
tabIndex: '0',
validations: {},
@ -198,7 +199,7 @@ InputSelectWithErrors.propTypes = {
}),
PropTypes.string,
]),
).isRequired,
),
style: PropTypes.object,
tabIndex: PropTypes.string,
validations: PropTypes.object,

View File

@ -510,6 +510,10 @@ module.exports = function (strapi) {
result.key = `where.${key}.$regex`;
result.value = value;
break;
case '_in':
result.key = `where.${key}.$in`;
result.value = value;
break;
default:
result = undefined;
}

View File

@ -0,0 +1,54 @@
/**
*
* CustomInputCheckbox
*/
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './styles.scss';
function CustomInputCheckbox({ isAll, name, onChange, value }) {
return (
<span className={cn('form-check', styles.customSpan)}>
<label
className={cn(
'form-check-label',
styles.customLabel,
isAll ? styles.customLabelHeader : styles.customLabelRow,
value && isAll && styles.customLabelCheckedHeader,
value && !isAll && styles.customLabelCheckedRow,
)}
htmlFor={name}
>
<input
className="form-check-input"
checked={value}
id={name}
name={name}
onChange={onChange}
type="checkbox"
/>
</label>
</span>
);
}
CustomInputCheckbox.defaultProps = {
isAll: false,
name: '',
value: false,
};
CustomInputCheckbox.propTypes = {
isAll: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
onChange: PropTypes.func.isRequired,
value: PropTypes.bool,
};
export default CustomInputCheckbox;

View File

@ -0,0 +1,62 @@
.customSpan {
margin-left: -15px;
}
.customLabel {
cursor: pointer;
> input {
display: none;
}
}
.customLabelHeader {
&:before {
content: '';
position: absolute;
left:15px; top: 5px;
width: 14px; height: 14px;
border: 1px solid rgba(16,22,34,0.15);
background-color: #FDFDFD;
border-radius: 3px;
}
}
.customLabelRow {
&:before {
content: '';
position: absolute;
left:15px; top: 19px;
width: 14px; height: 14px;
border: 1px solid rgba(16,22,34,0.15);
background-color: #FDFDFD;
border-radius: 3px;
}
}
.customLabelCheckedHeader {
&:after {
content: '\f00c';
position: absolute;
top: 5px; left: 17px;
font-size: 10px;
font-family: 'FontAwesome';
font-weight: 100;
color: #1C5DE7;
transition: all .2s;
}
}
.customLabelCheckedRow {
&:after {
content: '\f00c';
position: absolute;
top: 0px; left: 17px;
font-size: 10px;
font-family: 'FontAwesome';
font-weight: 100;
color: #1C5DE7;
transition: all .2s;
}
}

View File

@ -1,36 +1,86 @@
const FILTER_TYPES = [
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._lt',
value: '_lt',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._lte',
value: '_lte',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._gt',
value: '_gt',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._gte',
value: '_gte',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._contains',
value: '_contains',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._containss',
value: '_containss',
},
];
const getFilters = (type) => {
switch(type) {
case 'string':
case 'text':
case 'password':
case 'email':
return [
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._lt',
value: '_lt',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._lte',
value: '_lte',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._gt',
value: '_gt',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._gte',
value: '_gte',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._contains',
value: '_contains',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._containss',
value: '_containss',
},
];
case 'integer':
case 'float':
case 'decimal':
case 'date':
return [
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._lt',
value: '_lt',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._lte',
value: '_lte',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._gt',
value: '_gt',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._gte',
value: '_gte',
},
];
default:
return [
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'content-manager.components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
];
}
};
export default FILTER_TYPES;
export default getFilters;

View File

@ -17,7 +17,7 @@ import InputWithAutoFocus from './InputWithAutoFocus';
import Remove from './Remove';
import styles from './styles.scss';
import FILTER_TYPES from './filterTypes';
import getFilters from './filterTypes';
const defaultInputStyle = { width: '210px', marginRight: '10px', paddingTop: '4px' };
const midSelectStyle = { minWidth: '130px', maxWidth: '200px', marginLeft: '10px', marginRight: '10px' };
@ -51,7 +51,7 @@ function FilterOptions({ filter, filterToFocus, index, onChange, onClickAdd, onC
onChange={onChange}
name={`${index}.filter`}
value={get(filter, 'filter', '=')}
selectOptions={FILTER_TYPES}
selectOptions={getFilters(attrType)}
style={midSelectStyle}
/>
<div className={cn(isDate ? styles.filterOptionsInputWrapper : '')}>

View File

@ -6,10 +6,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { toString } from 'lodash';
import TableHeader from '../TableHeader';
import TableRow from '../TableRow';
import TableEmpty from '../TableEmpty';
import TableDelete from 'components/TableDelete';
import TableHeader from 'components/TableHeader';
import TableRow from 'components/TableRow';
import TableEmpty from 'components/TableEmpty';
import styles from './styles.scss';
@ -19,12 +21,13 @@ class Table extends React.Component {
(
<TableEmpty
filters={this.props.filters}
colspan={this.props.headers.length}
colspan={this.props.headers.length + 1}
contentType={this.props.routeParams.slug}
/>
) :
this.props.records.map((record, key) => (
<TableRow
onChange={this.props.onClickSelect}
key={key}
destination={`${this.props.route.path.replace(':slug', this.props.routeParams.slug)}/${record[this.props.primaryKey]}`}
headers={this.props.headers}
@ -33,18 +36,29 @@ class Table extends React.Component {
primaryKey={this.props.primaryKey}
onDelete={this.props.handleDelete}
redirectUrl={this.props.redirectUrl}
value={this.props.entriesToDelete.indexOf(toString(record.id)) !== -1}
/>
));
const entriesToDeleteNumber = this.props.entriesToDelete.length;
return (
<table className={`table ${styles.table}`}>
<TableHeader
onClickSelectAll={this.props.onClickSelectAll}
value={this.props.deleteAllValue}
headers={this.props.headers}
onChangeSort={this.props.onChangeSort}
sort={this.props.sort}
primaryKey={this.props.primaryKey}
/>
<tbody>
{ entriesToDeleteNumber > 0 && (
<TableDelete
colspan={this.props.headers.length + 1}
number={entriesToDeleteNumber}
onToggleDeleteAll={this.props.onToggleDeleteAll}
/>
)}
{rows}
</tbody>
</table>
@ -56,12 +70,22 @@ Table.contextTypes = {
router: PropTypes.object.isRequired,
};
Table.defaultProps = {
entriesToDelete: [],
handleDelete: () => {},
};
Table.propTypes = {
deleteAllValue: PropTypes.bool.isRequired,
entriesToDelete: PropTypes.array,
filters: PropTypes.array.isRequired,
handleDelete: PropTypes.func,
headers: PropTypes.array.isRequired,
history: PropTypes.object.isRequired,
onChangeSort: PropTypes.func.isRequired,
onClickSelect: PropTypes.func.isRequired,
onClickSelectAll: PropTypes.func.isRequired,
onToggleDeleteAll: PropTypes.func.isRequired,
primaryKey: PropTypes.string.isRequired,
records: PropTypes.oneOfType([
PropTypes.array,
@ -73,8 +97,4 @@ Table.propTypes = {
sort: PropTypes.string.isRequired,
};
Table.defaultProps = {
handleDelete: () => {},
};
export default Table;

View File

@ -0,0 +1,45 @@
/**
*
* TableDelete
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
function TableDelete({ colspan, number, onToggleDeleteAll }) {
const suffix = number > 1 ? 'plural' : 'singular';
return (
<tr className={styles.tableDelete}>
<td colSpan={colspan + 1}>
<FormattedMessage
id={`content-manager.components.TableDelete.entries.${suffix}`}
values={{ number }}
>
{message => <span className={styles.tableDeleteSpan}>{message}</span>}
</FormattedMessage>
<FormattedMessage
id="content-manager.components.TableDelete.delete"
>
{message => <span className={styles.deleteAll} onClick={onToggleDeleteAll}>{message}</span>}
</FormattedMessage>
</td>
</tr>
);
}
TableDelete.defaultProps = {
colspan: 0,
};
TableDelete.propTypes = {
colspan: PropTypes.number,
number: PropTypes.number.isRequired,
onToggleDeleteAll: PropTypes.func.isRequired,
};
export default TableDelete;

View File

@ -0,0 +1,44 @@
.tableDelete {
width: 100%;
height: 36px;
background: #F7F8F8;
td{
height: 36px;
line-height: 36px;
font-size: 1.3rem;
font-weight: 400;
color: #333740;
text-align: left;
border-collapse: collapse;
border-top: 1px solid #F1F1F2 !important;
}
}
.tableDeleteSpan {
font-weight: 600;
&:after {
content: '';
margin: 0 7px;
font-size: 13px;
font-weight: 600;
}
-webkit-font-smoothing: antialiased;
}
.deleteAll {
position: absolute;
color: #F64D0A;
font-weight: 500;
cursor: pointer;
&:after {
position: relative;
top: -1px;
content: '\f1f8';
margin-left: 7px;
// margin-top: -10px;
font-size: 13px;
font-family: FontAwesome;
-webkit-font-smoothing: antialiased;
}
}

View File

@ -7,6 +7,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import CustomInputCheckbox from 'components/CustomInputCheckbox';
import styles from './styles.scss';
class TableHeader extends React.Component {
@ -20,6 +22,17 @@ class TableHeader extends React.Component {
}
}
renderBulk = () => (
<th key="bulk_action">
<CustomInputCheckbox
isAll
name="all"
onChange={this.props.onClickSelectAll}
value={this.props.value}
/>
</th>
);
render() {
// Generate headers list
const headers = this.props.headers.map((header, i) => {
@ -52,18 +65,24 @@ class TableHeader extends React.Component {
return (
<thead className={styles.tableHeader}>
<tr >
{headers}
{[this.renderBulk()].concat(headers)}
</tr>
</thead>
);
}
}
TableHeader.defaultProps = {
value: false,
};
TableHeader.propTypes = {
headers: PropTypes.array.isRequired,
onChangeSort: PropTypes.func.isRequired,
onClickSelectAll: PropTypes.func.isRequired,
primaryKey: PropTypes.string.isRequired,
sort: PropTypes.string.isRequired,
value: PropTypes.bool,
};
export default TableHeader;

View File

@ -14,6 +14,12 @@
cursor: pointer;
}
}
> tr {
th:first-child {
width: 50px;
}
}
}
.iconAsc{

View File

@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
import moment from 'moment';
import { isEmpty, isObject, toString } from 'lodash';
import CustomInputCheckbox from 'components/CustomInputCheckbox';
import IcoContainer from 'components/IcoContainer';
import styles from './styles.scss';
@ -68,31 +69,51 @@ class TableRow extends React.Component {
this.context.router.history.push(`${this.props.destination}${this.props.redirectUrl}`);
}
renderAction = () => (
<td key='action' className={styles.actions}>
<IcoContainer
icons={[
{ icoType: 'pencil', onClick: () => this.handleClick(this.props.destination) },
{ id: this.props.record.id, icoType: 'trash', onClick: this.props.onDelete },
]}
/>
</td>
);
renderCells = () => {
const { headers } = this.props;
return [this.renderDelete()]
.concat(
headers.map((header, i) => (
<td key={i}>
<div className={styles.truncate}>
<div className={styles.truncated}>
{this.getDisplayedValue(
header.type,
this.props.record[header.name],
header.name,
)}
</div>
</div>
</td>
)))
.concat([this.renderAction()]);
}
renderDelete = () => (
<td onClick={(e) => e.stopPropagation()} key="i">
<CustomInputCheckbox
name={this.props.record.id}
onChange={this.props.onChange}
value={this.props.value}
/>
</td>
);
render() {
// Generate cells
const cells = this.props.headers.map((header, i) => (
<td key={i}>
<div className={styles.truncate}>
<div className={styles.truncated}>
{this.getDisplayedValue(
header.type,
this.props.record[header.name],
header.name,
)}
</div>
</div>
</td>
));
cells.push(
<td key='action' className={styles.actions}>
<IcoContainer icons={[{ icoType: 'pencil', onClick: () => this.handleClick(this.props.destination) }, { id: this.props.record.id, icoType: 'trash', onClick: this.props.onDelete }]} />
</td>
);
return (
<tr className={styles.tableRow} onClick={() => this.handleClick(this.props.destination)}>
{cells}
{this.renderCells()}
</tr>
);
}
@ -102,12 +123,18 @@ TableRow.contextTypes = {
router: PropTypes.object.isRequired,
};
TableRow.defaultProps = {
value: false,
};
TableRow.propTypes = {
destination: PropTypes.string.isRequired,
headers: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onDelete: PropTypes.func,
record: PropTypes.object.isRequired,
redirectUrl: PropTypes.string.isRequired,
value: PropTypes.bool,
};
TableRow.defaultProps = {

View File

@ -16,6 +16,9 @@
border-collapse: collapse;
border-top: 1px solid #F1F1F2 !important;
}
> td:first-child {
width: 50px;
}
}
.truncate {

View File

@ -9,10 +9,15 @@ import {
CHANGE_PARAMS,
DELETE_DATA,
DELETE_DATA_SUCCESS,
DELETE_SEVERAL_DATA,
DELETE_SEVERAL_DATA_SUCCESS,
GET_DATA,
GET_DATA_SUCCEEDED,
ON_CHANGE,
ON_CLICK_REMOVE,
ON_CLICK_SELECT,
ON_CLICK_SELECT_ALL,
ON_TOGGLE_DELETE_ALL,
ON_TOGGLE_FILTERS,
OPEN_FILTERS_WITH_SELECTION,
REMOVE_ALL_FILTERS,
@ -52,6 +57,21 @@ export function deleteDataSuccess(id) {
};
}
export function deleteSeveralData(entriesToDelete, model, source) {
return {
type: DELETE_SEVERAL_DATA,
entriesToDelete,
model,
source,
};
}
export function deleteSeveralDataSuccess() {
return {
type: DELETE_SEVERAL_DATA_SUCCESS,
};
}
export function getData(currentModel, source) {
return {
type: GET_DATA,
@ -83,6 +103,19 @@ export function onClickRemove(index) {
};
}
export function onClickSelect({ target }) {
return {
type: ON_CLICK_SELECT,
id: target.name,
};
}
export function onClickSelectAll() {
return {
type: ON_CLICK_SELECT_ALL,
};
}
export function openFiltersWithSelections(index) {
return {
type: OPEN_FILTERS_WITH_SELECTION,
@ -90,6 +123,12 @@ export function openFiltersWithSelections(index) {
};
}
export function onToggleDeleteAll() {
return {
type: ON_TOGGLE_DELETE_ALL,
};
}
export function onToggleFilters() {
return {
type: ON_TOGGLE_FILTERS,

View File

@ -9,10 +9,15 @@ export const ADD_FILTER = 'ContentManager/ListPage/ADD_FILTER';
export const CHANGE_PARAMS = 'ContentManager/ListPage/CHANGE_PARAMS';
export const DELETE_DATA = 'ContentManager/ListPage/DELETE_DATA';
export const DELETE_DATA_SUCCESS = 'ContentManager/ListPage/DELETE_DATA_SUCCESS';
export const DELETE_SEVERAL_DATA = 'ContentManager/ListPage/DELETE_SEVERAL_DATA';
export const DELETE_SEVERAL_DATA_SUCCESS = 'ContentManager/ListPage/DELETE_SEVERAL_DATA_SUCCESS';
export const GET_DATA = 'ContentManager/ListPage/GET_DATA';
export const GET_DATA_SUCCEEDED = 'ContentManager/ListPage/GET_DATA_SUCCEEDED';
export const ON_CHANGE = 'ContentManager/ListPage/ON_CHANGE';
export const ON_CLICK_REMOVE = 'ContentManager/ListPage/ON_CLICK_REMOVE';
export const ON_CLICK_SELECT = 'ContentManager/ListPage/ON_CLICK_SELECT';
export const ON_CLICK_SELECT_ALL = 'ContentManager/ListPage/ON_CLICK_SELECT_ALL';
export const ON_TOGGLE_DELETE_ALL = 'ContentManager/ListPage/ON_TOGGLE_DELETE_ALL';
export const ON_TOGGLE_FILTERS = 'ContentManager/ListPage/ON_TOGGLE_FILTERS';
export const OPEN_FILTERS_WITH_SELECTION = 'ContentManager/ListPage/OPEN_FILTERS_WITH_SELECTION';
export const REMOVE_ALL_FILTERS = 'ContentManager/ListPage/REMOVE_ALL_FILTERS';

View File

@ -39,9 +39,13 @@ import {
addFilter,
changeParams,
deleteData,
deleteSeveralData,
getData,
onChange,
onClickRemove,
onClickSelect,
onClickSelectAll,
onToggleDeleteAll,
onToggleFilters,
openFiltersWithSelections,
removeAllFilters,
@ -141,11 +145,11 @@ export class ListPage extends React.Component {
get(this.props.schema, [this.getCurrentModelName(), 'fields']) ||
get(this.props.schema, ['plugins', this.getSource(), this.getCurrentModelName(), 'fields']);
shouldHideFilters = () => {
if (this.props.listPage.showFilter) {
this.props.onToggleFilters();
}
};
getPopUpDeleteAllMsg = () => (
this.props.listPage.entriesToDelete.length > 1 ?
'content-manager.popUpWarning.bodyMessage.contentType.delete.all'
: 'content-manager.popUpWarning.bodyMessage.contentType.delete'
);
/**
* Generate the redirect URI when editing an entry
@ -185,6 +189,12 @@ export class ListPage extends React.Component {
return tableHeaders;
};
areAllEntriesSelected = () => {
const { listPage: { entriesToDelete, records } } = this.props;
return entriesToDelete.length === records.length && records.length > 0;
};
/**
* [findPageSort description]
* @param {Object} props [description]
@ -268,6 +278,12 @@ export class ListPage extends React.Component {
}
};
shouldHideFilters = () => {
if (this.props.listPage.showFilter) {
this.props.onToggleFilters();
}
};
toggleModalWarning = e => {
if (!isUndefined(e)) {
e.preventDefault();
@ -277,16 +293,32 @@ export class ListPage extends React.Component {
});
}
this.setState({ showWarning: !this.state.showWarning });
if (this.props.listPage.entriesToDelete.length > 0) {
this.props.onClickSelectAll();
}
this.setState(prevState => ({ showWarning: !prevState.showWarning }));
};
render() {
const {
addFilter,
deleteSeveralData,
listPage,
listPage: { appliedFilters, filters, filterToFocus, params, showFilter },
listPage: {
appliedFilters,
entriesToDelete,
filters,
filterToFocus,
params,
showFilter,
showWarningDeleteAll,
},
onChange,
onClickRemove,
onClickSelect,
onClickSelectAll,
onToggleDeleteAll,
onToggleFilters,
openFiltersWithSelections,
removeAllFilters,
@ -363,17 +395,22 @@ export class ListPage extends React.Component {
<div className={cn('row', styles.row)}>
<div className="col-md-12">
<Table
deleteAllValue={this.areAllEntriesSelected()}
entriesToDelete={entriesToDelete}
filters={filters}
handleDelete={this.toggleModalWarning}
headers={this.generateTableHeaders()}
history={this.props.history}
onChangeSort={this.handleChangeSort}
onClickSelectAll={onClickSelectAll}
onClickSelect={onClickSelect}
onToggleDeleteAll={onToggleDeleteAll}
primaryKey={this.getCurrentModel().primaryKey || 'id'}
records={listPage.records}
redirectUrl={this.generateRedirectURI()}
route={this.props.match}
routeParams={this.props.match.params}
headers={this.generateTableHeaders()}
filters={filters}
onChangeSort={this.handleChangeSort}
sort={params._sort}
history={this.props.history}
primaryKey={this.getCurrentModel().primaryKey || 'id'}
handleDelete={this.toggleModalWarning}
redirectUrl={this.generateRedirectURI()}
/>
<PopUpWarning
isOpen={this.state.showWarning}
@ -387,6 +424,20 @@ export class ListPage extends React.Component {
popUpWarningType="danger"
onConfirm={this.handleDelete}
/>
<PopUpWarning
isOpen={showWarningDeleteAll}
toggleModal={onToggleDeleteAll}
content={{
title: 'content-manager.popUpWarning.title',
message: this.getPopUpDeleteAllMsg(),
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm',
}}
popUpWarningType="danger"
onConfirm={() => {
deleteSeveralData(entriesToDelete, this.getCurrentModelName(), this.getSource());
}}
/>
<PageFooter
count={listPage.count}
onChangeParams={this.handleChangeParams}
@ -406,6 +457,7 @@ ListPage.propTypes = {
addFilter: PropTypes.func.isRequired,
changeParams: PropTypes.func.isRequired,
deleteData: PropTypes.func.isRequired,
deleteSeveralData: PropTypes.func.isRequired,
getData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
listPage: PropTypes.object.isRequired,
@ -414,6 +466,9 @@ ListPage.propTypes = {
models: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onClickRemove: PropTypes.func.isRequired,
onClickSelect: PropTypes.func.isRequired,
onClickSelectAll: PropTypes.func.isRequired,
onToggleDeleteAll: PropTypes.func.isRequired,
onToggleFilters: PropTypes.func.isRequired,
openFiltersWithSelections: PropTypes.func.isRequired,
removeAllFilters: PropTypes.func.isRequired,
@ -429,9 +484,13 @@ function mapDispatchToProps(dispatch) {
addFilter,
changeParams,
deleteData,
deleteSeveralData,
getData,
onChange,
onClickRemove,
onClickSelect,
onClickSelectAll,
onToggleDeleteAll,
onToggleFilters,
openFiltersWithSelections,
removeAllFilters,

View File

@ -5,26 +5,32 @@
*/
import { fromJS, List, Map } from 'immutable';
import { toString } from 'lodash';
// ListPage constants
import {
ADD_FILTER,
CHANGE_PARAMS,
DELETE_DATA_SUCCESS,
DELETE_SEVERAL_DATA_SUCCESS,
GET_DATA_SUCCEEDED,
ON_CHANGE,
ON_CLICK_REMOVE,
ON_CLICK_SELECT,
ON_CLICK_SELECT_ALL,
ON_TOGGLE_FILTERS,
OPEN_FILTERS_WITH_SELECTION,
REMOVE_ALL_FILTERS,
REMOVE_FILTER,
SET_PARAMS,
SUBMIT,
ON_TOGGLE_DELETE_ALL,
} from './constants';
const initialState = fromJS({
appliedFilters: List([]),
count: 0,
entriesToDelete: List([]),
filters: List([]),
filtersUpdated: false,
filterToFocus: null,
@ -35,6 +41,7 @@ const initialState = fromJS({
}),
records: List([]),
showFilter: false,
showWarningDeleteAll: false,
});
function listPageReducer(state = initialState, action) {
@ -53,10 +60,15 @@ function listPageReducer(state = initialState, action) {
})
))
.update('count', (v) => v = v - 1);
case DELETE_SEVERAL_DATA_SUCCESS:
return state
.update('showWarningDeleteAll', () => false)
.update('entriesToDelete', () => List([]));
case CHANGE_PARAMS:
return state.updateIn(action.keys, () => action.value);
case GET_DATA_SUCCEEDED:
return state
.update('entriesToDelete', () => List([]))
.update('count', () => action.data[0].count)
.update('records', () => List(action.data[1]));
case ON_CHANGE:
@ -66,6 +78,26 @@ function listPageReducer(state = initialState, action) {
.update('appliedFilters', list => list.splice(action.index, 1))
.update('filters', list => list.splice(action.index, 1))
.update('filtersUpdated', v => v = !v);
case ON_CLICK_SELECT:
return state.update('entriesToDelete', list => {
const index = state.get('entriesToDelete').indexOf(toString(action.id));
if (index !== -1) {
return list.splice(index, 1);
}
return list.concat(toString(action.id));
});
case ON_CLICK_SELECT_ALL:
return state.update('entriesToDelete', () => {
if (state.get('entriesToDelete').size === 0) {
return state
.get('records')
.reduce((acc, current) => acc.concat(List([toString(current.id)])), List([]));
}
return List([]);
});
case ON_TOGGLE_FILTERS:
return state
.update('filterToFocus', () => null)
@ -94,6 +126,8 @@ function listPageReducer(state = initialState, action) {
.update('appliedFilters', (list) => list.filter(filter => filter.get('value') !== ''))
.update('showFilter', () => false)
.update('filtersUpdated', v => v = !v);
case ON_TOGGLE_DELETE_ALL:
return state.update('showWarningDeleteAll', v => v = !v);
default:
return state;
}

View File

@ -16,11 +16,13 @@ import request from 'utils/request';
// Actions
import {
deleteDataSuccess,
deleteSeveralDataSuccess,
getDataSucceeded,
} from './actions';
// Constants
import {
DELETE_DATA,
DELETE_SEVERAL_DATA,
GET_DATA,
} from './constants';
// Selectors
@ -68,7 +70,7 @@ export function* dataGet(action) {
}
}
export function* dataDelete({ id, modelName, source}) {
export function* dataDelete({ id, modelName, source }) {
try {
const requestUrl = `/content-manager/explorer/${modelName}/${id}`;
const params = {};
@ -90,10 +92,26 @@ export function* dataDelete({ id, modelName, source}) {
}
}
export function* dataDeleteAll({ entriesToDelete, model, source }) {
try {
const params = Object.assign(entriesToDelete, source !== undefined ? { source } : {});
yield call(request, `/content-manager/explorer/deleteAll/${model}`, {
method: 'DELETE',
params,
});
yield put(deleteSeveralDataSuccess());
yield call(dataGet, { currentModel: model, source });
} catch(err) {
strapi.notification.error('content-manager.error.record.delete');
}
}
// All sagas to be loaded
function* defaultSaga() {
const loadDataWatcher = yield fork(takeLatest, GET_DATA, dataGet);
yield fork(takeLatest, DELETE_DATA, dataDelete);
yield fork(takeLatest, DELETE_SEVERAL_DATA, dataDeleteAll);
yield take(LOCATION_CHANGE);

View File

@ -33,6 +33,11 @@
"components.FilterOptions.FILTER_TYPES._contains": "contains",
"components.FilterOptions.FILTER_TYPES._containss": "contains (case sensitive)",
"components.TableDelete.entries.plural": "{number} entries selected",
"components.TableDelete.entries.singular": "{number} entry selected",
"components.TableDelete.delete": "Delete all",
"components.TableEmpty.withFilters": "There is no {contentType} with the applied filters...",
"components.TableEmpty.withoutFilter": "There is no {contentType}...",
@ -72,5 +77,6 @@
"popUpWarning.button.cancel": "Cancel",
"popUpWarning.button.confirm": "Confirm",
"popUpWarning.title": "Please confirm",
"popUpWarning.bodyMessage.contentType.delete": "Are you sure you want to delete this entry?"
"popUpWarning.bodyMessage.contentType.delete": "Are you sure you want to delete this entry?",
"popUpWarning.bodyMessage.contentType.delete.all": "Are you sure you want to delete theses entries?"
}

View File

@ -25,6 +25,10 @@
"components.FiltersPickWrapper.PluginHeader.title.filter": "Filtres",
"components.FiltersPickWrapper.hide": "Fermer",
"components.TableDelete.entries.plural": "{number} entrées sélectionnées",
"components.TableDelete.entries.singular": "{number} entrée sélectionnée",
"components.TableDelete.delete": "Tout supprimer",
"components.TableEmpty.withFilters": "Aucun {contentType} n'a été trouvé avec ces filtres...",
"components.TableEmpty.withoutFilter": "Aucun {contentType} n'a été trouvé...",
@ -71,5 +75,6 @@
"popUpWarning.button.cancel": "Annuler",
"popUpWarning.button.confirm": "Confirmer",
"popUpWarning.title": "Confirmation requise",
"popUpWarning.bodyMessage.contentType.delete": "Êtes-vous sûr de vouloir supprimer cette entrée ?"
"popUpWarning.bodyMessage.contentType.delete": "Êtes-vous sûr de vouloir supprimer cette entrée ?",
"popUpWarning.bodyMessage.contentType.delete.all": "Êtes-vous sûr de vouloir supprimer ces entrées ?"
}

View File

@ -1,10 +1,10 @@
const _ = require('lodash');
module.exports = {
find: async function (params, populate) {
find: async function (params, populate, raw = false) {
return this.query(function(qb) {
_.forEach(params.where, (where, key) => {
if (_.isArray(where.value)) {
if (_.isArray(where.value) && where.symbol !== 'IN') {
for (const value in where.value) {
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]);
}
@ -26,7 +26,7 @@ module.exports = {
}
}).fetchAll({
withRelated: populate || this.associations.map(x => x.alias)
});
}).then(data => raw ? data.toJSON() : data);
},
count: async function (params = {}) {
@ -128,5 +128,13 @@ module.exports = {
[this.primaryKey]: params.id
})
.destroy();
},
deleteMany: async function (params) {
return await this
.query(function(qb) {
return qb.whereIn('id', params.id);
})
.destroy();
}
};

View File

@ -1,13 +1,15 @@
const _ = require('lodash');
module.exports = {
find: async function (params, populate) {
return this
find: async function (params, populate, raw = false) {
const query = this
.find(params.where)
.limit(Number(params.limit))
.sort(params.sort)
.skip(Number(params.skip))
.populate(populate || this.associations.map(x => x.alias).join(' '));
return raw ? query.lean() : query;
},
count: async function (params) {
@ -77,5 +79,14 @@ module.exports = {
.remove({
[this.primaryKey]: params.id
});
},
deleteMany: async function (params) {
return this
.remove({
[this.primaryKey]: {
$in: params[this.primaryKey] || params.id
}
});
}
};

View File

@ -55,6 +55,14 @@
"policies": ["routing"]
}
},
{
"method": "DELETE",
"path": "/explorer/deleteAll/:model",
"handler": "ContentManager.deleteAll",
"config": {
"policies": ["routing"]
}
},
{
"method": "DELETE",
"path": "/explorer/:model/:id",

View File

@ -98,4 +98,8 @@ module.exports = {
delete: async ctx => {
ctx.body = await strapi.plugins['content-manager'].services['contentmanager'].delete(ctx.params, ctx.request.query);
},
deleteAll: async ctx => {
ctx.body = await strapi.plugins['content-manager'].services['contentmanager'].deleteMany(ctx.params, ctx.request.query);
}
};

View File

@ -159,4 +159,53 @@ module.exports = {
id: params.id
});
},
deleteMany: async (params, query) => {
const { source } = query;
const { model } = params;
const primaryKey = strapi.query(model, source).primaryKey;
const toRemove = Object.keys(query).reduce((acc, curr) => {
if (curr !== 'source') {
return acc.concat([query[curr]]);
}
return acc;
}, []);
const filters = strapi.utils.models.convertParams(model, { [`${primaryKey}_in`]: toRemove });
const entries = await strapi.query(model, source).find({ where: filters.where }, null, true);
const associations = strapi.query(model, source).associations;
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
associations.forEach(association => {
if (entry[association.alias]) {
switch (association.nature) {
case 'oneWay':
case 'oneToOne':
case 'manyToOne':
case 'oneToManyMorph':
entry[association.alias] = null;
break;
case 'oneToMany':
case 'manyToMany':
case 'manyToManyMorph':
entry[association.alias] = [];
break;
default:
}
}
});
await strapi.query(model, source).update({
[primaryKey]: entry[primaryKey],
values: _.pick(entry, associations.map(a => a.alias))
});
}
return strapi.query(model, source).deleteMany({
[primaryKey]: toRemove,
});
}
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="70" height="40" viewBox="0 0 70 40"><style>.st1{fill:#fff}</style><path fill="#f8b195" d="M67 40H3c-1.6 0-3-1.3-3-3V3c0-1.6 1.4-3 3-3h64c1.7 0 3 1.4 3 3v34c0 1.7-1.3 3-3 3z"/><circle cx="22" cy="10" r="2" class="st1"/><circle cx="22" cy="20" r="2" class="st1"/><circle cx="22" cy="30" r="2" class="st1"/><path d="M49 12H28c-1.1 0-2-.9-2-2s.9-2 2-2h21c1.1 0 2 .9 2 2s-.9 2-2 2zM49 22H28c-1.1 0-2-.9-2-2s.9-2 2-2h21c1.1 0 2 .9 2 2s-.9 2-2 2zM49 32H28c-1.1 0-2-.9-2-2s.9-2 2-2h21c1.1 0 2 .9 2 2s-.9 2-2 2z" class="st1"/></svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@ -18,6 +18,7 @@ import IcoNumber from '../../assets/images/icon_number.png';
import IcoRelation from '../../assets/images/icon_relation.png';
import IcoString from '../../assets/images/icon_string.png';
import IcoText from '../../assets/images/icon_text.png';
import IcoEnum from '../../assets/images/icon_enum.svg';
import styles from './styles.scss';
@ -34,6 +35,7 @@ const asset = {
'relation': IcoRelation,
'string': IcoString,
'text': IcoText,
'enum': IcoEnum,
};
function AttributeCard({ attribute, autoFocus, handleClick, tabIndex }) {

View File

@ -22,6 +22,7 @@ import IcoPassword from '../../assets/images/icon_password.png';
import IcoRelation from '../../assets/images/icon_relation.png';
import IcoString from '../../assets/images/icon_string.png';
import IcoText from '../../assets/images/icon_text.png';
import IcoEnum from '../../assets/images/icon_enum.svg';
import styles from './styles.scss';
/* eslint-disable jsx-a11y/no-static-element-interactions */
@ -45,8 +46,7 @@ class AttributeRow extends React.Component {
decimal: IcoNumber,
email: IcoEmail,
password: IcoPassword,
// TODO add Enumeration icon
enumeration: IcoJson,
enumeration: IcoEnum,
};
this.state = {
showWarning: false,

View File

@ -155,7 +155,7 @@ class PopUpForm extends React.Component { // eslint-disable-line react/prefer-st
<ModalBody className={styles.modalBody} style={modalBodyStyle}>
<form onSubmit={this.props.onSubmit}>
<div className="container-fluid">
<div className={`row ${this.props.renderModalBody ? 'justify-content-center' : ''}`}>
<div className="row">
{modalBody}
</div>
</div>

View File

@ -1,13 +1,14 @@
/**
*
* PopUpRelations
*
*/
*
* PopUpRelations
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { findIndex, get, isEmpty, map, take, takeRight } from 'lodash';
import { FormattedMessage } from 'react-intl';
import pluralize from 'pluralize';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import Input from 'components/InputsIndex';
@ -17,12 +18,21 @@ import RelationNaturePicker from 'components/RelationNaturePicker';
import styles from './styles.scss';
/* eslint-disable jsx-a11y/tabindex-no-positive */
class PopUpRelations extends React.Component { // eslint-disable-line react/prefer-stateless-function
class PopUpRelations extends React.Component {
// eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
this.popUpHeaderNavLinks = [
{ name: 'defineRelation', message: 'content-type-builder.popUpForm.navContainer.relation', nameToReplace: 'advancedSettings' },
{ name: 'advancedSettings', message: 'content-type-builder.popUpForm.navContainer.advanced', nameToReplace: 'defineRelation' },
{
name: 'defineRelation',
message: 'content-type-builder.popUpForm.navContainer.relation',
nameToReplace: 'advancedSettings',
},
{
name: 'advancedSettings',
message: 'content-type-builder.popUpForm.navContainer.advanced',
nameToReplace: 'defineRelation',
},
];
}
@ -33,12 +43,93 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
}
componentWillReceiveProps(nextProps) {
if (isEmpty(this.props.dropDownItems) && !isEmpty(nextProps.dropDownItems) && !this.props.isEditting) {
if (
isEmpty(this.props.dropDownItems) &&
!isEmpty(nextProps.dropDownItems) &&
!this.props.isEditting
) {
this.init(nextProps);
}
}
init = (props) => {
setPlaceholders = (firstCTName, secondCTName, relationType, values = this.props.values) => {
switch (relationType) {
case 'oneToMany':
firstCTName = pluralize(firstCTName);
break;
case 'manyToOne':
secondCTName = pluralize(secondCTName);
break;
case 'manyToMany':
firstCTName = pluralize(firstCTName);
secondCTName = pluralize(secondCTName);
break;
default:
// Do nothing
}
if (get(this.props.contentType, 'name') !== get(values, 'params.target')) {
this.props.onChange({ target: { name: 'name', value: firstCTName } });
this.props.onChange({ target: { name: 'params.key', value: secondCTName } });
this.props.resetFormErrors();
} else {
this.props.onChange({ target: { name: 'name', value: '' } });
this.props.onChange({ target: { name: 'params.key', value: '' } });
}
}
handleChange = e => {
this.props.onChange(e);
const shouldResetKeyParams = e.target.value === 'oneWay';
if (!this.props.isEditting && !shouldResetKeyParams) {
this.setPlaceholders(
get(this.props.values, ['params', 'target']),
get(this.props.contentType, 'name'),
e.target.value,
);
}
if (shouldResetKeyParams) {
this.props.onChange({ target: { name: 'params.key', value: '-' } });
}
}
handleClick = e => {
const value = e.target.id.split('.');
[
{
target: {
type: 'string',
value: value[0],
name: 'params.target',
},
},
{
target: {
type: 'string',
value: value[1] !== 'undefined' ? value[1] : '',
name: 'params.pluginValue',
},
},
].map(target => this.props.onChange(target));
if (!this.props.isEditting) {
if (get(this.props.contentType, 'name') !== value[0]) {
this.setPlaceholders(
value[0],
get(this.props.contentType, 'name'),
get(this.props.values, ['params', 'nature']),
value[0],
);
} else {
this.props.onChange({ target: { name: 'name', value: '' } });
this.props.onChange({ target: { name: 'params.key', value: '' } });
}
}
};
init = props => {
const target = {
name: 'params.target',
type: 'string',
@ -56,7 +147,14 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
},
});
}
}
if (get(props.contentType, 'name') !== get(props.dropDownItems, ['0', 'name'])) {
[
{ target: { name: 'name', value: get(props.dropDownItems, ['0', 'name']) } },
{ target: { name: 'params.key', value: get(props.contentType, 'name') } },
].map(target => this.props.onChange(target));
}
};
renderNavContainer = () => (
<div className={styles.navContainer}>
@ -70,7 +168,7 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
/>
))}
</div>
)
);
renderModalBodyAdvanced = () => (
<ModalBody className={`${styles.modalBodyAdvanced}`}>
@ -95,7 +193,10 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
<div className={styles.inputContainer}>
<div className="row">
{map(takeRight(this.props.form.items, 2), (value, index) => {
const addon = index === 0 ? get(this.props.values, 'name') : get(this.props.values, ['params', 'key']);
const addon =
index === 0
? get(this.props.values, 'name')
: get(this.props.values, ['params', 'key']);
return (
<Input
key={index}
@ -117,12 +218,25 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
</div>
</div>
</ModalBody>
)
);
renderModalBodyRelations = () => {
const header = get(this.props.values, ['params', 'pluginValue']) ?
get(this.props.dropDownItems, [findIndex(this.props.dropDownItems, {'name': get(this.props.values, ['params', 'target']), source: get(this.props.values, ['params', 'pluginValue']) })])
: get(this.props.dropDownItems, [findIndex(this.props.dropDownItems, ['name', get(this.props.values, ['params', 'target'])])]);
const header = get(this.props.values, ['params', 'pluginValue'])
? get(this.props.dropDownItems, [
findIndex(this.props.dropDownItems, {
name: get(this.props.values, ['params', 'target']),
source: get(this.props.values, ['params', 'pluginValue']),
}),
])
: get(this.props.dropDownItems, [
findIndex(this.props.dropDownItems, [
'name',
get(this.props.values, ['params', 'target']),
]),
]);
const errs = findIndex(this.props.formErrors, ['name',get(this.props.form, ['items', '0', 'name'])]) !== -1 ? this.props.formErrors[findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '0', 'name'])])].errors: [];
const errors = findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '1', 'name'])]) !== -1 ? this.props.formErrors[findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '1', 'name'])])].errors : [];
return (
<ModalBody className={`${styles.modalBody} ${styles.flex}`}>
@ -138,11 +252,11 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
onSubmit={this.props.onSubmit}
onChange={this.props.onChange}
didCheckErrors={this.props.didCheckErrors}
errors={findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '0', 'name'])]) !== -1 ? this.props.formErrors[findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '0', 'name'])])].errors : []}
errors={errs}
/>
<RelationNaturePicker
selectedIco={get(this.props.values, ['params', 'nature'])}
onChange={this.props.onChange}
onChange={this.handleChange}
contentTypeName={get(this.props.contentType, 'name')}
contentTypeTarget={get(this.props.values, ['params', 'target'])}
/>
@ -156,24 +270,46 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
value={get(this.props.values, ['params', 'key'])}
onChange={this.props.onChange}
didCheckErrors={this.props.didCheckErrors}
errors={findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '1', 'name'])]) !== -1 ? this.props.formErrors[findIndex(this.props.formErrors, ['name', get(this.props.form, ['items', '1', 'name'])])].errors : []}
errors={errors}
dropDownItems={this.props.dropDownItems}
onClick={this.handleClick}
/>
</ModalBody>
);
}
};
render() {
const loader = this.props.showLoader ?
<Button onClick={this.props.onSubmit} type="submit" className={styles.primary} disabled={this.props.showLoader}><p className={styles.saving}><span>.</span><span>.</span><span>.</span></p></Button>
: <Button type="submit" onClick={this.props.onSubmit} className={styles.primary}><FormattedMessage id="content-type-builder.form.button.continue" /></Button>;
const loader = this.props.showLoader ? (
<Button
onClick={this.props.onSubmit}
type="submit"
className={styles.primary}
disabled={this.props.showLoader}
>
<p className={styles.saving}>
<span>.</span>
<span>.</span>
<span>.</span>
</p>
</Button>
) : (
<Button type="submit" onClick={this.props.onSubmit} className={styles.primary}>
<FormattedMessage id="content-type-builder.form.button.continue" />
</Button>
);
const modalBody = this.props.showRelation ? this.renderModalBodyRelations(): this.renderModalBodyAdvanced();
const modalBody = this.props.showRelation
? this.renderModalBodyRelations()
: this.renderModalBodyAdvanced();
const handleToggle = this.props.toggle;
return (
<div className={styles.popUpRelations}>
<Modal isOpen={this.props.isOpen} toggle={this.props.toggle} className={`${styles.modalPosition}`}>
<Modal
isOpen={this.props.isOpen}
toggle={this.props.toggle}
className={`${styles.modalPosition}`}
>
<ModalHeader toggle={this.props.toggle} className={styles.popUpFormHeader} />
<div className={styles.headerContainer}>
<div className={styles.titleContainer}>
@ -190,7 +326,9 @@ class PopUpRelations extends React.Component { // eslint-disable-line react/pref
{modalBody}
<ModalFooter className={styles.modalFooter}>
<Button onClick={handleToggle} className={styles.secondary}><FormattedMessage id="content-type-builder.form.button.cancel" /></Button>
<Button onClick={handleToggle} className={styles.secondary}>
<FormattedMessage id="content-type-builder.form.button.cancel" />
</Button>
{loader}{' '}
</ModalFooter>
</Modal>
@ -203,19 +341,14 @@ PopUpRelations.propTypes = {
contentType: PropTypes.object,
didCheckErrors: PropTypes.bool.isRequired,
dropDownItems: PropTypes.array,
form: PropTypes.oneOfType([
PropTypes.array.isRequired,
PropTypes.object.isRequired,
]).isRequired,
formErrors: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
]).isRequired,
form: PropTypes.oneOfType([PropTypes.array.isRequired, PropTypes.object.isRequired]).isRequired,
formErrors: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
isEditting: PropTypes.bool,
isOpen: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
popUpTitle: PropTypes.string.isRequired,
resetFormErrors: PropTypes.func.isRequired,
routePath: PropTypes.string.isRequired,
showLoader: PropTypes.bool,
showRelation: PropTypes.bool.isRequired,

View File

@ -1,8 +1,8 @@
/**
*
* RelationBox
*
*/
*
* RelationBox
*
*/
import React from 'react';
import PropTypes from 'prop-types';
@ -16,7 +16,8 @@ import styles from './styles.scss';
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable react/jsx-wrap-multilines */
class RelationBox extends React.Component { // eslint-disable-line react/prefer-stateless-function
class RelationBox extends React.Component {
// eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
@ -25,58 +26,64 @@ class RelationBox extends React.Component { // eslint-disable-line react/prefer-
};
}
handleClick = (e) => {
const value = e.target.id.split('.');
const target = {
type: 'string',
value: value[0],
name: 'params.target',
};
this.props.onChange({ target });
this.props.onChange({
target: {
type: 'string',
value: value[1] !== 'undefined' ? value[1] : '',
name: 'params.pluginValue',
},
});
getPlaceholder = () => {
switch (true) {
case this.props.relationType === 'oneToMany' && this.props.isFirstContentType:
return pluralize(this.props.contentTypeTargetPlaceholder);
case this.props.relationType === 'manyToOne' && !this.props.isFirstContentType:
return pluralize(this.props.contentTypeTargetPlaceholder);
case this.props.relationType === 'manyToMany':
return pluralize(this.props.contentTypeTargetPlaceholder);
default:
return this.props.contentTypeTargetPlaceholder;
}
}
toggle = () => this.setState({ showMenu: !this.state.showMenu });
renderDropdownMenu = () => (
<div className={styles.dropDown}>
<ButtonDropdown isOpen={this.state.showMenu} toggle={this.toggle} style={{ backgroundColor: 'transparent' }}>
<DropdownToggle caret>
</DropdownToggle>
<ButtonDropdown
isOpen={this.state.showMenu}
toggle={this.toggle}
style={{ backgroundColor: 'transparent' }}
>
<DropdownToggle caret />
<DropdownMenu className={styles.dropDownContent}>
{map(this.props.dropDownItems, (value, key) => {
const id = value.source ? `${value.name}.${value.source}` : `${value.name}. `;
let divStyle;
if (get(this.props.header, 'name') === value.name && !isEmpty(get(this.props.header,'source')) && value.source) {
divStyle = { color: '#323740', fontWeight: 'bold'};
} else if (value.source === get(this.props.header, 'source') && value.name === get(this.props.header, 'name')) {
divStyle = { color: '#323740', fontWeight: 'bold'};
if (
get(this.props.header, 'name') === value.name &&
!isEmpty(get(this.props.header, 'source')) &&
value.source
) {
divStyle = { color: '#323740', fontWeight: 'bold' };
} else if (
value.source === get(this.props.header, 'source') &&
value.name === get(this.props.header, 'name')
) {
divStyle = { color: '#323740', fontWeight: 'bold' };
} else {
divStyle = { color: 'rgba(50,55,64,0.75)' };
}
return (
<div style={{ height: '3.6rem'}} key={key}>
<DropdownItem onClick={this.handleClick} id={id}>
<div style={{ height: '3.6rem' }} key={key}>
<DropdownItem onClick={this.props.onClick} id={id}>
<div style={divStyle} id={`${value.name}.${value.source}`}>
<i className={`fa ${value.icon}`} style={divStyle} id={id} />
{value.name}&nbsp;
{value.source ? (
{value.source && (
<FormattedMessage id="content-type-builder.from">
{(message) => (
<span style={{ fontStyle: 'italic' }} id={id}>({message}: {value.source})</span>
{message => (
<span style={{ fontStyle: 'italic' }} id={id}>
({message}: {value.source})
</span>
)}
</FormattedMessage>
) : ''}
)}
</div>
</DropdownItem>
</div>
@ -85,63 +92,41 @@ class RelationBox extends React.Component { // eslint-disable-line react/prefer-
</DropdownMenu>
</ButtonDropdown>
</div>
)
);
render() {
let placeholder;
switch (true) {
case this.props.relationType === 'oneToMany' && this.props.isFirstContentType:
placeholder = pluralize(this.props.contentTypeTargetPlaceholder);
break;
case this.props.relationType === 'manyToOne' && !this.props.isFirstContentType:
placeholder = pluralize(this.props.contentTypeTargetPlaceholder);
break;
case this.props.relationType === 'manyToMany':
placeholder = pluralize(this.props.contentTypeTargetPlaceholder);
break;
default:
placeholder = this.props.contentTypeTargetPlaceholder;
}
const content = isEmpty(this.props.input) ?
<div /> :
<Input
disabled={this.props.relationType === 'oneWay' && this.props.tabIndex === '2'}
tabIndex={this.props.tabIndex}
type={get(this.props.input, 'type')}
onChange={this.props.onChange}
label={get(this.props.input, 'label')}
name={get(this.props.input, 'name')}
value={this.props.value}
placeholder={placeholder}
customBootstrapClass="col-md-12"
validations={get(this.props.input, 'validations')}
errors={this.props.errors}
didCheckErrors={this.props.didCheckErrors}
pluginID="content-type-builder"
autoFocus={this.props.autoFocus}
/>;
const dropDown = !isEmpty(this.props.dropDownItems) ? this.renderDropdownMenu() : '';
return (
<div className={styles.relationBox}>
<div className={styles.headerContainer}>
<i className={`fa ${get(this.props.header, 'icon')}`} />
{startCase(get(this.props.header, 'name'))}&nbsp;
<span style={{ fontStyle: 'italic', fontWeight: '500' }}>
{get(this.props.header, 'source') ? (
`(${get(this.props.header, 'source')})`
): ''}
{get(this.props.header, 'source') ? `(${get(this.props.header, 'source')})` : ''}
</span>
{dropDown}
{!isEmpty(this.props.dropDownItems) && this.renderDropdownMenu()}
</div>
<div className={styles.inputContainer}>
<form onSubmit={this.props.onSubmit}>
<div className="container-fluid">
<div className={`row ${styles.input}`}>
{content}
{!isEmpty(this.props.input) && (
<Input
disabled={this.props.relationType === 'oneWay' && this.props.tabIndex === '2'}
tabIndex={this.props.tabIndex}
type={get(this.props.input, 'type')}
onChange={this.props.onChange}
label={get(this.props.input, 'label')}
name={get(this.props.input, 'name')}
value={this.props.value}
placeholder={this.getPlaceholder()}
customBootstrapClass="col-md-12"
validations={get(this.props.input, 'validations')}
errors={this.props.errors}
didCheckErrors={this.props.didCheckErrors}
pluginID="content-type-builder"
autoFocus={this.props.autoFocus}
/>
)}
</div>
</div>
</form>
@ -161,6 +146,7 @@ RelationBox.propTypes = {
input: PropTypes.object,
isFirstContentType: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func,
onSubmit: PropTypes.func.isRequired,
relationType: PropTypes.string,
tabIndex: PropTypes.string.isRequired,
@ -175,6 +161,7 @@ RelationBox.defaultProps = {
header: {},
input: {},
isFirstContentType: false,
onClick: () => {},
relationType: 'oneToOne',
value: '',
};

View File

@ -115,6 +115,10 @@
{
"type": "relation",
"description": "content-type-builder.popUpForm.attributes.relation.description"
},
{
"type": "enumeration",
"description": "content-type-builder.popUpForm.attributes.enumeration.description"
}
]
},
@ -912,6 +916,84 @@
}
]
}
},
"enumeration": {
"baseSettings": {
"items": [
{
"label": {
"id": "content-type-builder.form.attribute.item.enumeration.name"
},
"name": "name",
"type": "string",
"value": ""
},
{
"label": {
"id": "content-type-builder.form.attribute.item.enumeration.rules"
},
"name": "params.enumValue",
"type": "textarea",
"placeholder": "content-type-builder.form.attribute.item.enumeration.placeholder",
"value": false,
"validations": {
"required": true
}
}
]
},
"advancedSettings": {
"items": [
{
"label": {
"id": "content-type-builder.form.attribute.settings.default"
},
"name": "params.default",
"type": "string",
"value": "",
"validations": {}
},
{
"label": {
"id": "content-type-builder.form.attribute.item.enumeration.graphql"
},
"name": "params.enumName",
"type": "string",
"value": "",
"validations": {},
"inputDescription": {
"id": "content-type-builder.form.attribute.item.enumeration.graphql.description"
}
},
{
"title": {
"id": "content-type-builder.form.attribute.item.settings.name"
},
"label": {
"id": "content-type-builder.form.attribute.item.requiredField"
},
"name": "params.required",
"type": "checkbox",
"value": false,
"validations": {},
"inputDescription": {
"id": "content-type-builder.form.attribute.item.requiredField.description"
}
},
{
"label": {
"id": "content-type-builder.form.attribute.item.uniqueField"
},
"name": "params.unique",
"type": "checkbox",
"value": false,
"validations": {},
"inputDescription": {
"id": "content-type-builder.form.attribute.item.uniqueField.description"
}
}
]
}
}
}
}

View File

@ -384,6 +384,11 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
handleChange = ({ target }) => {
let value = target.type === 'number' && target.value !== '' ? toNumber(target.value) : target.value;
// Parse enumeration textarea to transform it into a array
if (target.name === 'params.enumValue') {
value = target.value.split(',');
}
if (isObject(target.value) && target.value._isAMomentObject === true) {
value = moment(target.value, 'YYYY-MM-DD HH:mm:ss').format();
}
@ -391,14 +396,12 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
if (includes(this.props.hash.split('::')[1], 'attribute')) {
this.props.changeInputAttribute(target.name, value);
if (target.name === 'params.nature' && target.value === "manyToMany") {
if (target.name === 'params.nature' && target.value === 'manyToMany') {
this.props.changeInputAttribute('params.dominant', true);
}
if (target.name === 'params.nature' && target.value === "oneWay") {
if (target.name === 'params.nature' && target.value === 'oneWay') {
this.props.changeInputAttribute('params.key', '-');
}else if (target.name === 'params.nature'){
this.props.changeInputAttribute('params.key', '');
}
} else {
@ -539,7 +542,6 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
}
render() {
// Ensure typeof(popUpFormType) is String
const popUpFormType = split(this.props.hash, '::')[1] || '';
const popUpTitle = this.generatePopUpTitle(popUpFormType);
@ -576,6 +578,7 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
formErrors={this.props.formErrors}
didCheckErrors={this.props.didCheckErrors}
isEditting={edit}
resetFormErrors={this.props.resetFormErrors}
/>
);
}

View File

@ -79,7 +79,9 @@ function formReducer(state = initialState, action) {
.update('formErrors', (list) => list.splice(findIndex(state.get('formErrors').toJS(), ['target', 'name']), 1))
.set('didCheckErrors', !state.get('didCheckErrors'));
case RESET_FORM_ERRORS:
return state.set('formErrors', List());
return state
.update('didCheckErrors', v => v = !v)
.set('formErrors', List());
case RESET_IS_FORM_SET:
return state
.set('isFormSet', false)

View File

@ -146,9 +146,12 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
}
handleDelete = (attributeName) => {
const index = findIndex(this.props.modelPage.model.attributes, ['name', attributeName]);
const parallelAttributeIndex = findIndex(this.props.modelPage.model.attributes, (attr) => attr.params.key === attributeName);
const { modelPage: { model } } = this.props;
const index = findIndex(model.attributes, ['name', attributeName]);
const attributeToRemove = get(model, ['attributes', index]);
const parallelAttributeIndex = attributeToRemove.name === attributeToRemove.params.key ?
-1 : findIndex(model.attributes, (attr) => attr.params.key === attributeName);
this.props.deleteAttribute(index, this.props.match.params.modelName, parallelAttributeIndex !== -1);
}
@ -158,7 +161,7 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
// Display a notification if the attribute is not present in the ones that the ctb handles
if (!has(attribute.params, 'nature') && !includes(availableAttributes, attribute.params.type)) {
return strapi.notification.info('content-type-builder.notification.info.enumeration');
return strapi.notification.info('content-type-builder.notification.info.disable');
}
const settingsType = attribute.params.type ? 'baseSettings' : 'defineRelation';
const parallelAttributeIndex = findIndex(this.props.modelPage.model.attributes, ['name', attribute.params.key]);

View File

@ -1,164 +1,173 @@
{
"plugin.description.short": "Modelliere die Datenstruktur deiner API.",
"plugin.description.long": "Modelliere die Datenstruktur deiner API. Lege neue Felder und Beziehungen innerhalb von einer Minute an. Erforderliche Dateien werden automatisch in deinem Projekt angelegt und aktualisiert.",
"attribute.string": "String",
"attribute.text": "Text",
"attribute.boolean": "Boolean",
"attribute.float": "Float",
"attribute.integer": "Integer",
"attribute.decimal": "Decimal",
"attribute.date": "Datum",
"attribute.json": "JSON",
"attribute.media": "Medien",
"attribute.email": "E-Mail",
"attribute.password": "Passwort",
"attribute.relation": "Beziehung",
"attribute.enumeration": "Enumeration",
"plugin.description.short": "Modelliere die Datenstruktur deiner API.",
"plugin.description.long":
"Modelliere die Datenstruktur deiner API. Lege neue Felder und Beziehungen innerhalb von einer Minute an. Erforderliche Dateien werden automatisch in deinem Projekt angelegt und aktualisiert.",
"attribute.string": "String",
"attribute.text": "Text",
"attribute.boolean": "Boolean",
"attribute.float": "Float",
"attribute.integer": "Integer",
"attribute.decimal": "Decimal",
"attribute.date": "Datum",
"attribute.json": "JSON",
"attribute.media": "Medien",
"attribute.email": "E-Mail",
"attribute.password": "Passwort",
"attribute.relation": "Beziehung",
"attribute.enumeration": "Enumeration",
"attribute.WYSIWYG": "Text (WYSIWYG)",
"contentType.temporaryDisplay": "(Nicht gespeichert)",
"from": "aus",
"home.contentTypeBuilder.name": "Content-Typen",
"home.contentTypeBuilder.description": "Verwalte deine Content-Typen.",
"home.emptyContentType.title": "Es sind keine Content-Typen verfügbar",
"home.emptyContentType.description": "Lege deinen ersten Content-Typ an, Daten deiner API abrufen zu können.",
"contentType.temporaryDisplay": "(Nicht gespeichert)",
"from": "aus",
"home.contentTypeBuilder.name": "Content-Typen",
"home.contentTypeBuilder.description": "Verwalte deine Content-Typen.",
"home.emptyContentType.title": "Es sind keine Content-Typen verfügbar",
"home.emptyContentType.description": "Lege deinen ersten Content-Typ an, Daten deiner API abrufen zu können.",
"home.emptyAttributes.title": "Es gibt noch keine Felder",
"home.emptyAttributes.description": "Füge deinem Content-Typen das erste Feld hinzu",
"home.emptyAttributes.title": "Es gibt noch keine Felder",
"home.emptyAttributes.description": "Füge deinem Content-Typen das erste Feld hinzu",
"button.contentType.create": "Lege einen Content-Typ an",
"button.contentType.add": "Neuer Content-Typ",
"button.attributes.add": "Neues Feld",
"button.contentType.create": "Lege einen Content-Typ an",
"button.contentType.add": "Neuer Content-Typ",
"button.attributes.add": "Neues Feld",
"error.validation.required": "Dieser Wert ist erforderlich.",
"error.validation.regex": "Dieser Wert entspricht nicht dem RegEx.",
"error.validation.max": "Dieser Wert ist zu hoch.",
"error.validation.min": "Dieser Wert ist zu niedrig.",
"error.validation.maxLength": "Dieser Wert ist zu lang.",
"error.validation.minLength": "Dieser Wert ist zu kurz.",
"error.contentTypeName.taken": "Dieser Name existiert bereits",
"error.attribute.taken": "Dieser Feldname ist bereits vergeben",
"error.attribute.key.taken": "Dieser Wert existiert bereits",
"error.attribute.sameKeyAndName": "Darf nicht gleich sein",
"error.validation.minSupMax": "Darf nicht höher sein",
"error.validation.required": "Dieser Wert ist erforderlich.",
"error.validation.regex": "Dieser Wert entspricht nicht dem RegEx.",
"error.validation.max": "Dieser Wert ist zu hoch.",
"error.validation.min": "Dieser Wert ist zu niedrig.",
"error.validation.maxLength": "Dieser Wert ist zu lang.",
"error.validation.minLength": "Dieser Wert ist zu kurz.",
"error.contentTypeName.taken": "Dieser Name existiert bereits",
"error.attribute.taken": "Dieser Feldname ist bereits vergeben",
"error.attribute.key.taken": "Dieser Wert existiert bereits",
"error.attribute.sameKeyAndName": "Darf nicht gleich sein",
"error.validation.minSupMax": "Darf nicht höher sein",
"form.attribute.item.textarea.name": "Name",
"form.attribute.item.number.name": "Name",
"form.attribute.item.date.name": "Name",
"form.attribute.item.media.name": "Name",
"form.attribute.item.media.multiple": "Erlaube mehrere Dateien",
"form.attribute.item.json.name": "Name",
"form.attribute.item.boolean.name": "Name",
"form.attribute.item.string.name": "Name",
"form.attribute.item.settings.name": "Einstellungen",
"form.attribute.item.requiredField": "Benötigtes Feld",
"form.attribute.item.uniqueField": "Einzigartiges Feld",
"form.attribute.item.minimum": "Mindestwert",
"form.attribute.item.minimumLength": "Mindestlänge",
"form.attribute.item.maximumLength": "Maximallänge",
"form.attribute.item.maximum": "Maximalwert",
"form.attribute.item.requiredField.description": "Du wirst keinen Eintrag anlegen können, wenn dieses Feld leer ist",
"form.attribute.item.uniqueField.description": "Du wirst keinen Eintrag anlegen können, wenn es bereits einen Eintrag mit identischem Inhalt gibt",
"form.attribute.item.defineRelation.fieldName": "Feldname",
"form.attribute.item.customColumnName": "Eigener Spaltenname",
"form.attribute.item.customColumnName.description": "Dies ist nützlich, um Spalten in der Datenbank für Antworten der API umzubenennen",
"form.attribute.item.number.type": "Zahlenformat",
"form.attribute.item.number.type.integer": "integer (z.B.: 10)",
"form.attribute.item.number.type.float": "float (z.B.: 3.33333333)",
"form.attribute.item.number.type.decimal": "decimal (z.B.: 2.22)",
"form.attribute.settings.default": "Standardwert",
"form.attribute.settings.default.checkboxLabel": "Set to true",
"form.attribute.item.textarea.name": "Name",
"form.attribute.item.number.name": "Name",
"form.attribute.item.date.name": "Name",
"form.attribute.item.media.name": "Name",
"form.attribute.item.media.multiple": "Erlaube mehrere Dateien",
"form.attribute.item.json.name": "Name",
"form.attribute.item.boolean.name": "Name",
"form.attribute.item.string.name": "Name",
"form.attribute.item.enumeration.name": "Name",
"form.attribute.item.enumeration.rules": "Werte (trennen Sie sie mit einem Komma)",
"form.attribute.item.enumeration.placeholder": "Ex: Morgen, Mittag, Abend",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.settings.name": "Einstellungen",
"form.attribute.item.requiredField": "Benötigtes Feld",
"form.attribute.item.uniqueField": "Einzigartiges Feld",
"form.attribute.item.minimum": "Mindestwert",
"form.attribute.item.minimumLength": "Mindestlänge",
"form.attribute.item.maximumLength": "Maximallänge",
"form.attribute.item.maximum": "Maximalwert",
"form.attribute.item.requiredField.description": "Du wirst keinen Eintrag anlegen können, wenn dieses Feld leer ist",
"form.attribute.item.uniqueField.description": "Du wirst keinen Eintrag anlegen können, wenn es bereits einen Eintrag mit identischem Inhalt gibt",
"form.attribute.item.defineRelation.fieldName": "Feldname",
"form.attribute.item.customColumnName": "Eigener Spaltenname",
"form.attribute.item.customColumnName.description": "Dies ist nützlich, um Spalten in der Datenbank für Antworten der API umzubenennen",
"form.attribute.item.number.type": "Zahlenformat",
"form.attribute.item.number.type.integer": "integer (z.B.: 10)",
"form.attribute.item.number.type.float": "float (z.B.: 3.33333333)",
"form.attribute.item.number.type.decimal": "decimal (z.B.: 2.22)",
"form.attribute.settings.default": "Standardwert",
"form.attribute.settings.default.checkboxLabel": "Set to true",
"form.button.cancel": "Abbrechen",
"form.button.continue": "Weiter",
"form.button.save": "Speichern",
"form.button.cancel": "Abbrechen",
"form.button.continue": "Weiter",
"form.button.save": "Speichern",
"form.contentType.item.connections": "Verbindung",
"form.contentType.item.name": "Name",
"form.contentType.item.name.description": "Der Name des Content-Typs sollte Singular sein. {link}",
"form.contentType.item.name.link.description": "Schau dir unsere Dokumentation an.",
"form.contentType.item.description": "Beschreibung",
"form.contentType.item.description.placeholder": "Beschreibe deinen Content-Typ",
"form.contentType.item.collectionName": "Name des Dokuments in der Datenbank",
"form.contentType.item.collectionName.inputDescription": "Nützlich, wenn Content-Typ und Datenbankname unterschiedlich sind",
"form.contentType.item.connections": "Verbindung",
"form.contentType.item.name": "Name",
"form.contentType.item.name.description": "Der Name des Content-Typs sollte Singular sein. {link}",
"form.contentType.item.name.link.description": "Schau dir unsere Dokumentation an.",
"form.contentType.item.description": "Beschreibung",
"form.contentType.item.description.placeholder": "Beschreibe deinen Content-Typ",
"form.contentType.item.collectionName": "Name des Dokuments in der Datenbank",
"form.contentType.item.collectionName.inputDescription": "Nützlich, wenn Content-Typ und Datenbankname unterschiedlich sind",
"menu.section.contentTypeBuilder.name.plural": "Content-Typen",
"menu.section.contentTypeBuilder.name.singular": "Content-Typ",
"menu.section.documentation.name": "Dokumentation",
"menu.section.documentation.guide": "Mehr über Content-Typen findest du in unserer",
"menu.section.documentation.guideLink": "Anleitung.",
"menu.section.documentation.tutorial": "Schau dir unser",
"menu.section.documentation.tutorialLink": "Tutorial an.",
"menu.section.contentTypeBuilder.name.plural": "Content-Typen",
"menu.section.contentTypeBuilder.name.singular": "Content-Typ",
"menu.section.documentation.name": "Dokumentation",
"menu.section.documentation.guide": "Mehr über Content-Typen findest du in unserer",
"menu.section.documentation.guideLink": "Anleitung.",
"menu.section.documentation.tutorial": "Schau dir unser",
"menu.section.documentation.tutorialLink": "Tutorial an.",
"modelPage.contentHeader.emptyDescription.description": "Dieser Content-Typ hat keine Beschreibung",
"modelPage.contentType.list.title.plural": "Felder",
"modelPage.contentType.list.title.singular": "Feld",
"modelPage.contentType.list.title.including": "schließt ein",
"modelPage.contentType.list.relationShipTitle.plural": "Beziehungen",
"modelPage.contentType.list.relationShipTitle.singular": "Beziehung",
"modelPage.attribute.relationWith": "Beziehung mit",
"modelPage.contentHeader.emptyDescription.description": "Dieser Content-Typ hat keine Beschreibung",
"modelPage.contentType.list.title.plural": "Felder",
"modelPage.contentType.list.title.singular": "Feld",
"modelPage.contentType.list.title.including": "schließt ein",
"modelPage.contentType.list.relationShipTitle.plural": "Beziehungen",
"modelPage.contentType.list.relationShipTitle.singular": "Beziehung",
"modelPage.attribute.relationWith": "Beziehung mit",
"noTableWarning.description": "Vergiss nicht, die Tabelle `{modelName}` in deiner Datenbank zu erstellen",
"noTableWarning.infos": "Mehr Informationen",
"noTableWarning.description": "Vergiss nicht, die Tabelle `{modelName}` in deiner Datenbank zu erstellen",
"noTableWarning.infos": "Mehr Informationen",
"notification.error.message": "Ein Fehler ist aufgetreten",
"notification.info.contentType.creating.notSaved": "Bitte speichere zuerst diesen Content-Typ bevor du einen neuen anlegst",
"notification.info.optimized": "Dieses Plugin ist auf deinen localStorage optimiert",
"notification.success.message.contentType.edit": "Der Content-Typ wurde aktualisiert",
"notification.success.message.contentType.create": "Der Content-Typ wurde angelegt",
"notification.success.contentTypeDeleted": "Der Content-Typ wurde gelöscht",
"notification.info.enumeration": "Dieses Feld ist momentan nicht editierbar...😮",
"notification.error.message": "Ein Fehler ist aufgetreten",
"notification.info.contentType.creating.notSaved": "Bitte speichere zuerst diesen Content-Typ bevor du einen neuen anlegst",
"notification.info.disable": "Dieses Feld ist momentan nicht editierbar...😮",
"notification.info.optimized": "Dieses Plugin ist auf deinen localStorage optimiert",
"notification.success.message.contentType.edit": "Der Content-Typ wurde aktualisiert",
"notification.success.message.contentType.create": "Der Content-Typ wurde angelegt",
"notification.success.contentTypeDeleted": "Der Content-Typ wurde gelöscht",
"popUpForm.attributes.string.description": "Titel, Namen, Namenslisten",
"popUpForm.attributes.text.description": "Beschreibungen, Paragraphen, Artikel",
"popUpForm.attributes.boolean.description": "Ja/Nein, 1 oder 0, Wahr/Falsch",
"popUpForm.attributes.number.description": "Jegliche Zahlen",
"popUpForm.attributes.date.description": "Event-Daten, Öffnungszeiten",
"popUpForm.attributes.json.description": "Daten in JSON-Format",
"popUpForm.attributes.media.description": "Bilder, Videos, PDFs und andere",
"popUpForm.attributes.relation.description": "Bezieht sich auf einen Content-Typ",
"popUpForm.attributes.email.description": "E-Mail-Adressen von Benutzern",
"popUpForm.attributes.password.description": "Passwörter von Benutzers",
"popUpForm.attributes.string.description": "Titel, Namen, Namenslisten",
"popUpForm.attributes.text.description": "Beschreibungen, Paragraphen, Artikel",
"popUpForm.attributes.boolean.description": "Ja/Nein, 1 oder 0, Wahr/Falsch",
"popUpForm.attributes.number.description": "Jegliche Zahlen",
"popUpForm.attributes.date.description": "Event-Daten, Öffnungszeiten",
"popUpForm.attributes.json.description": "Daten in JSON-Format",
"popUpForm.attributes.media.description": "Bilder, Videos, PDFs und andere",
"popUpForm.attributes.relation.description": "Bezieht sich auf einen Content-Typ",
"popUpForm.attributes.email.description": "E-Mail-Adressen von Benutzern",
"popUpForm.attributes.password.description": "Passwörter von Benutzers",
"popUpForm.attributes.enumeration.description": "Liste der Auswahlmöglichkeiten",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.boolean.name": "Boolean",
"popUpForm.attributes.date.name": "Datum",
"popUpForm.attributes.json.name": "JSON",
"popUpForm.attributes.media.name": "Medien",
"popUpForm.attributes.number.name": "Zahl",
"popUpForm.attributes.relation.name": "Beziehung",
"popUpForm.attributes.email.name": "E-Mail",
"popUpForm.attributes.password.name": "Passwort",
"popUpForm.attributes.enumeration.name": "Enumeration",
"popUpForm.create": "Neu",
"popUpForm.edit": "Bearbeiten",
"popUpForm.field": "Feld",
"popUpForm.create.contentType.header.title": "Neuer Content-Typ",
"popUpForm.choose.attributes.header.title": "Neues Feld hinzufügen",
"popUpForm.edit.contentType.header.title": "Content-Typen bearbeiten",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.boolean.name": "Boolean",
"popUpForm.attributes.date.name": "Datum",
"popUpForm.attributes.json.name": "JSON",
"popUpForm.attributes.media.name": "Medien",
"popUpForm.attributes.number.name": "Zahl",
"popUpForm.attributes.relation.name": "Beziehung",
"popUpForm.attributes.email.name": "E-Mail",
"popUpForm.attributes.password.name": "Passwort",
"popUpForm.create": "Neu",
"popUpForm.edit": "Bearbeiten",
"popUpForm.field": "Feld",
"popUpForm.create.contentType.header.title": "Neuer Content-Typ",
"popUpForm.choose.attributes.header.title": "Neues Feld hinzufügen",
"popUpForm.edit.contentType.header.title": "Content-Typen bearbeiten",
"popUpForm.navContainer.relation": "Beziehung definieren",
"popUpForm.navContainer.base": "Grundeinstellungen",
"popUpForm.navContainer.advanced": "Fortgeschrittene Einstellungen",
"popUpForm.navContainer.relation": "Beziehung definieren",
"popUpForm.navContainer.base": "Grundeinstellungen",
"popUpForm.navContainer.advanced": "Fortgeschrittene Einstellungen",
"popUpRelation.title": "Beziehung",
"popUpRelation.title": "Beziehung",
"popUpWarning.button.cancel": "Abbrechen",
"popUpWarning.button.confirm": "Bestätigen",
"popUpWarning.title": "Bitte bestätigen",
"popUpWarning.bodyMessage.contentType.delete": "Bist du sicher, dass du diesen Content-Typ löschen willst?",
"popUpWarning.bodyMessage.attribute.delete": "Bist du sicher, dass du dieses Feld löschen willst?",
"popUpWarning.button.cancel": "Abbrechen",
"popUpWarning.button.confirm": "Bestätigen",
"popUpWarning.title": "Bitte bestätigen",
"popUpWarning.bodyMessage.contentType.delete": "Bist du sicher, dass du diesen Content-Typ löschen willst?",
"popUpWarning.bodyMessage.attribute.delete": "Bist du sicher, dass du dieses Feld löschen willst?",
"table.contentType.title.plural": "Content-Typen sind verfügbar",
"table.contentType.title.singular": "Content-Typ ist verfügbar",
"table.contentType.head.name": "Name",
"table.contentType.head.description": "Beschreibung",
"table.contentType.head.fields": "Felder",
"table.contentType.title.plural": "Content-Typen sind verfügbar",
"table.contentType.title.singular": "Content-Typ ist verfügbar",
"table.contentType.head.name": "Name",
"table.contentType.head.description": "Beschreibung",
"table.contentType.head.fields": "Felder",
"relation.oneToOne": "hat ein(en)",
"relation.oneToMany": "gehört zu vielen",
"relation.manyToOne": "hat viele",
"relation.manyToMany": "hat und gehört zu vielen",
"relation.attributeName.placeholder": "z.B.: Autor, Kategorie"
"relation.oneToOne": "hat ein(en)",
"relation.oneToMany": "gehört zu vielen",
"relation.manyToOne": "hat viele",
"relation.manyToMany": "hat und gehört zu vielen",
"relation.attributeName.placeholder": "z.B.: Autor, Kategorie"
}
}

View File

@ -53,6 +53,11 @@
"form.attribute.item.json.name": "Name",
"form.attribute.item.boolean.name": "Name",
"form.attribute.item.string.name": "Name",
"form.attribute.item.enumeration.name": "Name",
"form.attribute.item.enumeration.rules": "Values (separate them with a comma)",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.enumeration.placeholder": "Ex: morning,noon,evening",
"form.attribute.item.appearance.name": "Appearance",
"form.attribute.item.appearance.label": "Display as a WYSIWYG",
"form.attribute.item.appearance.description":
@ -116,11 +121,11 @@
"notification.error.message": "An error occurred",
"notification.info.contentType.creating.notSaved":
"Please save your current Content Type before creating a new one",
"notification.info.disable": "This field is not editable for the moment...😮",
"notification.info.optimized": "This plugin is optimized with your localStorage",
"notification.success.message.contentType.edit": "Your Content Type has been updated",
"notification.success.message.contentType.create": "Your Content Type has been created",
"notification.success.contentTypeDeleted": "The Content Type has been deleted",
"notification.info.enumeration": "This field is not editable for the moment...😮",
"popUpForm.attributes.string.description": "Titles, names, paragraphs, list of names",
"popUpForm.attributes.text.description": "Descriptions, text paragraphs, articles ",
@ -132,6 +137,7 @@
"popUpForm.attributes.relation.description": "Refers to a Content Type",
"popUpForm.attributes.email.description": "User's email...",
"popUpForm.attributes.password.description": "User password...",
"popUpForm.attributes.enumeration.description": "List of choices",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
@ -143,6 +149,7 @@
"popUpForm.attributes.relation.name": "Relation",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "Password",
"popUpForm.attributes.enumeration.name": "Enumeration",
"popUpForm.create": "Add New",
"popUpForm.edit": "Edit",
"popUpForm.field": "Field",

View File

@ -1,20 +1,20 @@
{
"plugin.description.short": "Modelisez la structure de données de votre API.",
"plugin.description.short": "Modélisez la structure de données de votre API.",
"plugin.description.long":
"Modelisez la structure de données de votre API. Créer des nouveaux champs et relations en un instant. Les fichiers se créent et se mettent à jour automatiquement.",
"Modélisez la structure de données de votre API. Créer des nouveaux champs et relations en un instant. Les fichiers se créent et se mettent à jour automatiquement.",
"attribute.string": "Chaîne de caractères",
"attribute.text": "Text",
"attribute.text": "Texte",
"attribute.boolean": "Booléen",
"attribute.float": "Décimal approximatif",
"attribute.integer": "Entier",
"attribute.decimal": "Décimal",
"attribute.date": "Date",
"attribute.json": "JSON",
"attribute.media": "Media",
"attribute.media": "Média",
"attribute.password": "Mot de passe",
"attribute.email": "Email",
"attribute.relation": "Relation",
"attribute.enumeration": "Enumération",
"attribute.enumeration": "Énumération",
"attribute.WYSIWYG": "Text (WYSIWYG)",
"contentType.temporaryDisplay": "(Non sauvegardé)",
@ -26,7 +26,7 @@
"home.emptyContentType.title": "Il n'y a pas de model disponible",
"home.emptyContentType.description": "Créez votre premier modèle...",
"home.emptyAttributes.title": "Il n'y a pas encore de champs",
"home.emptyAttributes.title": "Il n'y a pas encore de champ",
"home.emptyAttributes.description": "Ajoutez votre premier champ a votre modèle",
"button.contentType.create": "Créer un modèle",
@ -54,6 +54,11 @@
"form.attribute.item.media.name": "Nom",
"form.attribute.item.media.multiple": "Peut être relié à plusieurs fichiers",
"form.attribute.item.string.name": "Nom",
"form.attribute.item.enumeration.name": "Nom",
"form.attribute.item.enumeration.rules": "Valeurs (les séparer par une virgule)",
"form.attribute.item.enumeration.graphql": "Surchage du nom pour GraphQL",
"form.attribute.item.enumeration.graphql.description": "Vous permet de remplacer le nom généré par défaut pour GraphQL",
"form.attribute.item.enumeration.placeholder": "Ex: matin,midi,soir",
"form.attribute.item.appearance.name": "Apparence",
"form.attribute.item.appearance.label": "Editable avec un WYSIWYG",
"form.attribute.item.appearance.description":
@ -118,11 +123,11 @@
"notification.error.message": "Une erreur est survenue",
"notification.info.contentType.creating.notSaved":
"Sauvegardez votre Modèle en cours avant d'en créer un nouveau",
"notification.info.disable": "Ce champ n'est pas modifiable pour le moment...😮",
"notification.info.optimized": "Ce plugin est optimisé pour votre localStorage",
"notification.success.message.contentType.edit": "Votre modèle a bien été modifié",
"notification.success.message.contentType.create": "Votre modèle a bien été créée",
"notification.success.contentTypeDeleted": "Le modèle a bien été supprimé.",
"notification.info.enumeration": "Ce champs n'est pas modifiable pour le moment...😮",
"popUpForm.attributes.string.description": "Titres, noms,...",
"popUpForm.attributes.text.description": "Descriptions, paragraphes texte, articles ",
@ -134,22 +139,24 @@
"popUpForm.attributes.relation.description": "Pointe vers un autre Modèle",
"popUpForm.attributes.password.description": "Mot de passe utilisateur...",
"popUpForm.attributes.email.description": "Email utilisateurs",
"popUpForm.attributes.enumeration.description": "Liste de choix",
"popUpForm.attributes.string.name": "Chaîne de caractères",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.text.name": "Texte",
"popUpForm.attributes.boolean.name": "Booléen",
"popUpForm.attributes.number.name": "Nombre",
"popUpForm.attributes.date.name": "Date",
"popUpForm.attributes.json.name": "JSON",
"popUpForm.attributes.media.name": "Media",
"popUpForm.attributes.media.name": "Média",
"popUpForm.attributes.relation.name": "Relation",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "Mot de passe",
"popUpForm.attributes.enumeration": "Énumération",
"popUpForm.create": "Ajouter un Nouveau",
"popUpForm.edit": "Modifer",
"popUpForm.field": "Champ",
"popUpForm.create.contentType.header.title": "Ajouter un Nouveau Modèle",
"popUpForm.choose.attributes.header.title": "Ajouter un Nouveau Champs",
"popUpForm.choose.attributes.header.title": "Ajouter un Nouveau Champ",
"popUpForm.edit.contentType.header.title": "Modifier un Modèle",
"popUpForm.navContainer.relation": "Définir relation",
"popUpForm.navContainer.base": "Réglages de base",

View File

@ -52,6 +52,11 @@
"form.attribute.item.json.name": "Nazwa",
"form.attribute.item.boolean.name": "Nazwa",
"form.attribute.item.string.name": "Nazwa",
"form.attribute.item.enumeration.name": "Nazwa",
"form.attribute.item.enumeration.rules": "Values (separate them with a comma)",
"form.attribute.item.enumeration.placeholder": "Ex: morning,noon,evening",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.settings.name": "Ustawienia",
"form.attribute.item.requiredField": "Wymagany",
"form.attribute.item.uniqueField": "Unikalny",
@ -110,11 +115,11 @@
"notification.error.message": "Wystąpił błąd",
"notification.info.contentType.creating.notSaved":
"Zapisz proszę aktualny model zanim stworzysz nowy",
"notification.info.disable": "Tego pola nie można obecnie edytować... 😮",
"notification.info.optimized": "Ta wtyczka jest zoptymalizowana z localStorage",
"notification.success.message.contentType.edit": "Model został zmieniony",
"notification.success.message.contentType.create": "Model został utworzony",
"notification.success.contentTypeDeleted": "Model został usunięty",
"notification.info.enumeration": "Tego pola nie można obecnie edytować... 😮",
"popUpForm.attributes.string.description": "Tytuły, nazwy, paragrafy, lista nazwisk",
"popUpForm.attributes.text.description": "Opisy, paragrafy, artykuły ",
@ -126,7 +131,8 @@
"popUpForm.attributes.relation.description": "Odnosi się do modelu",
"popUpForm.attributes.email.description": "Email użytkownika...",
"popUpForm.attributes.password.description": "Hasło użytkownika...",
"popUpForm.attributes.enumeration.description": "List of choices",
"popUpForm.attributes.string.name": "Ciąg",
"popUpForm.attributes.text.name": "Tekst",
"popUpForm.attributes.boolean.name": "Typ logiczny",
@ -137,6 +143,7 @@
"popUpForm.attributes.relation.name": "Relacja",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "Hasło",
"popUpForm.attributes.enumeration.name": "Wyliczenie",
"popUpForm.create": "Nowy",
"popUpForm.edit": "Zmień",
"popUpForm.field": "Atrybut",

View File

@ -1,176 +1,183 @@
{
"plugin.description.short": "Моделируйте структуру данных вашего API.",
"plugin.description.long":
"Моделируйте структуру данных вашего API. Создавайте новые поля и связи всего за минуту. Файлы автоматически создаются и обновляются в вашем проекте.",
"attribute.string": "String",
"attribute.text": "Text",
"attribute.boolean": "Boolean",
"attribute.float": "Float",
"attribute.integer": "integer",
"attribute.decimal": "Decimal",
"attribute.date": "Date",
"attribute.json": "JSON",
"attribute.media": "Media",
"attribute.email": "Email",
"attribute.password": "Password",
"attribute.relation": "Связь",
"attribute.enumeration": "Enumeration",
"attribute.WYSIWYG": "Text (WYSIWYG)",
"plugin.description.short": "Моделируйте структуру данных вашего API.",
"plugin.description.long":
"Моделируйте структуру данных вашего API. Создавайте новые поля и связи всего за минуту. Файлы автоматически создаются и обновляются в вашем проекте.",
"attribute.string": "String",
"attribute.text": "Text",
"attribute.boolean": "Boolean",
"attribute.float": "Float",
"attribute.integer": "Integer",
"attribute.decimal": "Decimal",
"attribute.date": "Date",
"attribute.json": "JSON",
"attribute.media": "Media",
"attribute.email": "Email",
"attribute.password": "Password",
"attribute.relation": "Связь",
"attribute.enumeration": "Enumeration",
"attribute.WYSIWYG": "Text (WYSIWYG)",
"contentType.temporaryDisplay": "(Не сохранено)",
"from": "from",
"home.contentTypeBuilder.name": "Типы Данных",
"home.contentTypeBuilder.description": "Создавайте и обновляйте ваш Тип Данных.",
"home.emptyContentType.title": "Нет Типов Данных",
"home.emptyContentType.description":
"Создайте ваш первый Тип Данных и у вас появится возможность загружать ваши данные при помощи API.",
"home.emptyAttributes.title": "Пока ни одного поля не создано",
"home.emptyAttributes.description": "Добавте первое поле в ваш новый Тип Данных",
"button.contentType.create": "Создать Тип Данных",
"button.contentType.add": "Добавить Тип Данных",
"button.attributes.add": "Добавить Новое Поле",
"error.validation.required": "Это поле является обязательным.",
"error.validation.regex": "Не соответствует регулярному выражению.",
"error.validation.max": "Слишком большое.",
"error.validation.min": "Слишком маленькое.",
"error.validation.maxLength": "Слишком длинное.",
"error.validation.minLength": "Слишком короткое.",
"error.contentTypeName.taken": "Это название уже существует",
"error.attribute.taken": "Поле с таким названием уже существует",
"error.attribute.key.taken": "Это значение уже существует",
"error.attribute.sameKeyAndName": "Не может быть одинаковым",
"error.validation.minSupMax": "Не может быть выше",
"form.attribute.item.textarea.name": "Название",
"form.attribute.item.number.name": "Название",
"form.attribute.item.date.name": "Название",
"form.attribute.item.media.name": "Название",
"form.attribute.item.media.multiple": "Возможно несколько файлов",
"form.attribute.item.json.name": "Название",
"form.attribute.item.boolean.name": "Название",
"form.attribute.item.string.name": "Название",
"form.attribute.item.enumeration.name": "Название",
"form.attribute.item.enumeration.rules": "Values (separate them with a comma)",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.enumeration.placeholder": "Ex: morning,noon,evening",
"form.attribute.item.appearance.name": "Отображение",
"form.attribute.item.appearance.label": "Показывать WYSIWYG",
"form.attribute.item.appearance.description":
"В противном случае значение будет доступно для редактирования как обычное текстовое поле",
"form.attribute.item.settings.name": "Настройки",
"form.attribute.item.requiredField": "Обязательное поле",
"form.attribute.item.uniqueField": "Уникальное поле",
"form.attribute.item.minimum": "Минимальное значение",
"form.attribute.item.minimumLength": "Минимальная длина",
"form.attribute.item.maximumLength": "Максимальная длина",
"form.attribute.item.maximum": "Максимальное значение",
"form.attribute.item.requiredField.description":
"Вы не сможете создать запись если это поле останенься пустым",
"form.attribute.item.uniqueField.description":
"Вы не сможете создать запись если существует запись с аналогичным содержанием",
"form.attribute.item.defineRelation.fieldName": "Название поля",
"form.attribute.item.customColumnName": "Настраиваемые названия столбца",
"form.attribute.item.customColumnName.description":
"Это удобно иметь возможность переименовывать название столбцов для настройки ответов от API.",
"form.attribute.item.number.type": "Числовой формат",
"form.attribute.item.number.type.integer": "integer (ex: 10)",
"form.attribute.item.number.type.float": "float (ex: 3.33333333)",
"form.attribute.item.number.type.decimal": "decimal (ex: 2.22)",
"form.attribute.settings.default": "Стандартное значение",
"form.attribute.settings.default.checkboxLabel": "Установить значение — true",
"form.button.cancel": "Отменить",
"form.button.continue": "Продолжить",
"form.button.save": "Сохранить",
"form.contentType.item.connections": "Соединение",
"form.contentType.item.name": "Название",
"form.contentType.item.name.description": "Название Типов Данных должны быть уникальными: {link}",
"form.contentType.item.name.link.description": "Ознакомьтесь с нашей документацией",
"form.contentType.item.description": "Описание",
"form.contentType.item.description.placeholder": "Добавте ваше короткое описание...",
"form.contentType.item.collectionName": "Название коллекции",
"form.contentType.item.collectionName.inputDescription":
"Полезно, когда название вашего Типа Данных и название вашей таблицы различаются",
"menu.section.contentTypeBuilder.name.plural": "Типы Данных",
"menu.section.contentTypeBuilder.name.singular": "Тип Данных",
"menu.section.documentation.name": "Документация",
"menu.section.documentation.guide": "Прочтите больше о Типах Данных в нашем",
"menu.section.documentation.guideLink": "руководстве.",
"menu.section.documentation.tutorial": "Посмотрите наши",
"menu.section.documentation.tutorialLink": "обучающие видео.",
"modelPage.contentHeader.emptyDescription.description":
"Нет описания для этого Типа Данных",
"modelPage.contentType.list.title.plural": "поля",
"modelPage.contentType.list.title.singular": "поле",
"modelPage.contentType.list.title.including": "включает",
"modelPage.contentType.list.relationShipTitle.plural": "связи",
"modelPage.contentType.list.relationShipTitle.singular": "связь",
"modelPage.attribute.relationWith": "Связан с",
"noTableWarning.description": "Не забудте создать таблицу `{modelName}` в вашей базе данных",
"noTableWarning.infos": "Больше информации",
"notification.error.message": "Возникла ошибка",
"notification.info.contentType.creating.notSaved":
"Пожалуйста сохраните ваш текущий Тип Данных перед тем как создавать новый",
"notification.info.disable": "Это поле в данный момент не редактируемо...😮",
"notification.info.optimized": "Плагин оптимизирован с вашим localstorage",
"notification.success.message.contentType.edit": "Ваш Тип Данных обновлен",
"notification.success.message.contentType.create": "Ваш Тип Данных создан",
"notification.success.contentTypeDeleted": "Ваш Тип Данных удален",
"popUpForm.attributes.string.description": "Загаловки, названия, имена, перечень названий",
"popUpForm.attributes.text.description": "Описания, текстовые параграфы, статьи",
"popUpForm.attributes.boolean.description": "Yes или no, 1 или 0, true или false",
"popUpForm.attributes.number.description": "Все что является числом",
"popUpForm.attributes.date.description": "Дата события, рабочие часы",
"popUpForm.attributes.json.description": "Данные в JSON формате",
"popUpForm.attributes.media.description": "Картинки, видео, PDF и другие виды файлов",
"popUpForm.attributes.relation.description": "Связан с Типом Данных",
"popUpForm.attributes.email.description": "Пользовательский email...",
"popUpForm.attributes.password.description": "Пароль пользователя...",
"popUpForm.attributes.enumeration.description": "List of choices",
"contentType.temporaryDisplay": "(Не сохранено)",
"from": "from",
"home.contentTypeBuilder.name": "Типы Данных",
"home.contentTypeBuilder.description": "Создавайте и обновляйте ваш Тип Данных.",
"home.emptyContentType.title": "Нет Типов Данных",
"home.emptyContentType.description":
"Создайте ваш первый Тип Данных и у вас появится возможность загружать ваши данные при помощи API.",
"home.emptyAttributes.title": "Пока ни одного поля не создано",
"home.emptyAttributes.description": "Добавте первое поле в ваш новый Тип Данных",
"button.contentType.create": "Создать Тип Данных",
"button.contentType.add": "Добавить Тип Данных",
"button.attributes.add": "Добавить Новое Поле",
"error.validation.required": "Это поле является обязательным.",
"error.validation.regex": "Не соответствует регулярному выражению.",
"error.validation.max": "Слишком большое.",
"error.validation.min": "Слишком маленькое.",
"error.validation.maxLength": "Слишком длинное.",
"error.validation.minLength": "Слишком короткое.",
"error.contentTypeName.taken": "Это название уже существует",
"error.attribute.taken": "Поле с таким названием уже существует",
"error.attribute.key.taken": "Это значение уже существует",
"error.attribute.sameKeyAndName": "Не может быть одинаковым",
"error.validation.minSupMax": "Не может быть выше",
"form.attribute.item.textarea.name": "Название",
"form.attribute.item.number.name": "Название",
"form.attribute.item.date.name": "Название",
"form.attribute.item.media.name": "Название",
"form.attribute.item.media.multiple": "Возможно несколько файлов",
"form.attribute.item.json.name": "Название",
"form.attribute.item.boolean.name": "Название",
"form.attribute.item.string.name": "Название",
"form.attribute.item.appearance.name": "Отображение",
"form.attribute.item.appearance.label": "Показывать WYSIWYG",
"form.attribute.item.appearance.description":
"В противном случае значение будет доступно для редактирования как обычное текстовое поле",
"form.attribute.item.settings.name": "Настройки",
"form.attribute.item.requiredField": "Обязательное поле",
"form.attribute.item.uniqueField": "Уникальное поле",
"form.attribute.item.minimum": "Минимальное значение",
"form.attribute.item.minimumLength": "Минимальная длина",
"form.attribute.item.maximumLength": "Максимальная длина",
"form.attribute.item.maximum": "Максимальное значение",
"form.attribute.item.requiredField.description":
"Вы не сможете создать запись если это поле останенься пустым",
"form.attribute.item.uniqueField.description":
"Вы не сможете создать запись если существует запись с аналогичным содержанием",
"form.attribute.item.defineRelation.fieldName": "Название поля",
"form.attribute.item.customColumnName": "Настраиваемые названия столбца",
"form.attribute.item.customColumnName.description":
"Это удобно иметь возможность переименовывать название столбцов для настройки ответов от API.",
"form.attribute.item.number.type": "Числовой формат",
"form.attribute.item.number.type.integer": "integer (ex: 10)",
"form.attribute.item.number.type.float": "float (ex: 3.33333333)",
"form.attribute.item.number.type.decimal": "decimal (ex: 2.22)",
"form.attribute.settings.default": "Стандартное значение",
"form.attribute.settings.default.checkboxLabel": "Установить значение — true",
"form.button.cancel": "Отменить",
"form.button.continue": "Продолжить",
"form.button.save": "Сохранить",
"form.contentType.item.connections": "Соединение",
"form.contentType.item.name": "Название",
"form.contentType.item.name.description": "Название Типов Данных должны быть уникальными: {link}",
"form.contentType.item.name.link.description": "Ознакомьтесь с нашей документацией",
"form.contentType.item.description": "Описание",
"form.contentType.item.description.placeholder": "Добавте ваше короткое описание...",
"form.contentType.item.collectionName": "Название коллекции",
"form.contentType.item.collectionName.inputDescription":
"Полезно, когда название вашего Типа Данных и название вашей таблицы различаются",
"menu.section.contentTypeBuilder.name.plural": "Типы Данных",
"menu.section.contentTypeBuilder.name.singular": "Тип Данных",
"menu.section.documentation.name": "Документация",
"menu.section.documentation.guide": "Прочтите больше о Типах Данных в нашем",
"menu.section.documentation.guideLink": "руководстве.",
"menu.section.documentation.tutorial": "Посмотрите наши",
"menu.section.documentation.tutorialLink": "обучающие видео.",
"modelPage.contentHeader.emptyDescription.description":
"Нет описания для этого Типа Данных",
"modelPage.contentType.list.title.plural": "поля",
"modelPage.contentType.list.title.singular": "поле",
"modelPage.contentType.list.title.including": "включает",
"modelPage.contentType.list.relationShipTitle.plural": "связи",
"modelPage.contentType.list.relationShipTitle.singular": "связь",
"modelPage.attribute.relationWith": "Связан с",
"noTableWarning.description": "Не забудте создать таблицу `{modelName}` в вашей базе данных",
"noTableWarning.infos": "Больше информации",
"notification.error.message": "Возникла ошибка",
"notification.info.contentType.creating.notSaved":
"Пожалуйста сохраните ваш текущий Тип Данных перед тем как создавать новый",
"notification.info.optimized": "Плагин оптимизирован с вашим localstorage",
"notification.success.message.contentType.edit": "Ваш Тип Данных обновлен",
"notification.success.message.contentType.create": "Ваш Тип Данных создан",
"notification.success.contentTypeDeleted": "Ваш Тип Данных удален",
"notification.info.enumeration": "Это поле в данный момент не редактируемо...😮",
"popUpForm.attributes.string.description": "Загаловки, названия, имена, перечень названий",
"popUpForm.attributes.text.description": "Описания, текстовые параграфы, статьи",
"popUpForm.attributes.boolean.description": "Yes или no, 1 или 0, true или false",
"popUpForm.attributes.number.description": "Все что является числом",
"popUpForm.attributes.date.description": "Дата события, рабочие часы",
"popUpForm.attributes.json.description": "Данные в JSON формате",
"popUpForm.attributes.media.description": "Картинки, видео, PDF и другие виды файлов",
"popUpForm.attributes.relation.description": "Связан с Типом Данных",
"popUpForm.attributes.email.description": "Пользовательский email...",
"popUpForm.attributes.password.description": "Пароль пользователя...",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.boolean.name": "Boolean",
"popUpForm.attributes.date.name": "Date",
"popUpForm.attributes.json.name": "JSON",
"popUpForm.attributes.media.name": "Media",
"popUpForm.attributes.number.name": "Number",
"popUpForm.attributes.relation.name": "Relation",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "Password",
"popUpForm.create": "Добавить новое",
"popUpForm.edit": "Отредактировать",
"popUpForm.field": "Поле",
"popUpForm.create.contentType.header.title": "Добавить новый Тип Данных",
"popUpForm.choose.attributes.header.title": "Добавить новое поле",
"popUpForm.edit.contentType.header.title": "Отредактировать Тип Данных",
"popUpForm.navContainer.relation": "Определить связь",
"popUpForm.navContainer.base": "Базовый настройки",
"popUpForm.navContainer.advanced": "Расширенные настройки",
"popUpRelation.title": "Связь",
"popUpWarning.button.cancel": "Отменить",
"popUpWarning.button.confirm": "Подтвердить",
"popUpWarning.title": "Пожалуйста подтвердите",
"popUpWarning.bodyMessage.contentType.delete":
"Вы уверены, что хотите удалить этот Тип Данных?",
"popUpWarning.bodyMessage.attribute.delete": "Вы уверены, что хотите удалить это поле?",
"table.contentType.title.plural": "Типы Данных доступны",
"table.contentType.title.singular": "Тип Данных доступен",
"table.contentType.head.name": "Название",
"table.contentType.head.description": "Описание",
"table.contentType.head.fields": "Поля",
"relation.oneToOne": "имеет один",
"relation.oneToMany": "принадлежит многим",
"relation.manyToOne": "имеет много",
"relation.manyToMany": "имеет и принадлежит многим",
"relation.attributeName.placeholder": "Пример: автор, категория, тег"
}
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.boolean.name": "Boolean",
"popUpForm.attributes.date.name": "Date",
"popUpForm.attributes.json.name": "JSON",
"popUpForm.attributes.media.name": "Media",
"popUpForm.attributes.number.name": "Number",
"popUpForm.attributes.relation.name": "Relation",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "Password",
"popUpForm.attributes.enumeration.name": "Enumeration",
"popUpForm.create": "Добавить новое",
"popUpForm.edit": "Отредактировать",
"popUpForm.field": "Поле",
"popUpForm.create.contentType.header.title": "Добавить новый Тип Данных",
"popUpForm.choose.attributes.header.title": "Добавить новое поле",
"popUpForm.edit.contentType.header.title": "Отредактировать Тип Данных",
"popUpForm.navContainer.relation": "Определить связь",
"popUpForm.navContainer.base": "Базовый настройки",
"popUpForm.navContainer.advanced": "Расширенные настройки",
"popUpRelation.title": "Связь",
"popUpWarning.button.cancel": "Отменить",
"popUpWarning.button.confirm": "Подтвердить",
"popUpWarning.title": "Пожалуйста подтвердите",
"popUpWarning.bodyMessage.contentType.delete":
"Вы уверены, что хотите удалить этот Тип Данных?",
"popUpWarning.bodyMessage.attribute.delete": "Вы уверены, что хотите удалить это поле?",
"table.contentType.title.plural": "Типы Данных доступны",
"table.contentType.title.singular": "Тип Данных доступен",
"table.contentType.head.name": "Название",
"table.contentType.head.description": "Описание",
"table.contentType.head.fields": "Поля",
"relation.oneToOne": "имеет один",
"relation.oneToMany": "принадлежит многим",
"relation.manyToOne": "имеет много",
"relation.manyToMany": "имеет и принадлежит многим",
"relation.attributeName.placeholder": "Пример: автор, категория, тег"
}

View File

@ -52,6 +52,11 @@
"form.attribute.item.json.name": "İsim",
"form.attribute.item.boolean.name": "İsim",
"form.attribute.item.string.name": "İsim",
"form.attribute.item.enumeration.name": "İsim",
"form.attribute.item.enumeration.rules": "Values (separate them with a comma)",
"form.attribute.item.enumeration.placeholder": "Ex: morning,noon,evening",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.appearance.name": "Görünüm",
"form.attribute.item.appearance.label": "WYSIWYG olarak görüntüle",
"form.attribute.item.appearance.description":
@ -114,11 +119,11 @@
"notification.error.message": "Bir hata oluştu.",
"notification.info.contentType.creating.notSaved":
"Lütfen yeni bir tane oluşturmadan önce mevcut İçerik Türünüzü kaydedin",
"notification.info.disable": "This field is not editable for the moment...😮",
"notification.info.optimized": "Bu eklenti, localStorage ile optimize edilmiştir",
"notification.success.message.contentType.edit": "İçerik Türünüz güncellendi",
"notification.success.message.contentType.create": "İçerik Türünüz oluşturuldu",
"notification.success.contentTypeDeleted": "İçerik Türü silindi",
"notification.info.enumeration": "This field is not editable for the moment...😮",
"popUpForm.attributes.string.description": "Başlıklar, adlar, paragraflar, isim listesi",
"popUpForm.attributes.text.description": "Tanımlar, metin paragrafları, makaleler ",
@ -130,7 +135,8 @@
"popUpForm.attributes.relation.description": "Bir İçerik Türünü Belirtiyor",
"popUpForm.attributes.email.description": "Kullanıcının e-postası...",
"popUpForm.attributes.password.description": "Kullanıcı şifresi...",
"popUpForm.attributes.enumeration.description": "List of choices",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Yazı",
"popUpForm.attributes.boolean.name": "Mantıksal",
@ -141,6 +147,7 @@
"popUpForm.attributes.relation.name": "İlişki",
"popUpForm.attributes.email.name": "E-posta",
"popUpForm.attributes.password.name": "Parola",
"popUpForm.attributes.enumeration.name": "Enumeration",
"popUpForm.create": "Yeni ekle",
"popUpForm.edit": "Düzenle",
"popUpForm.field": "Alan",

View File

@ -52,6 +52,11 @@
"form.attribute.item.json.name": "Name",
"form.attribute.item.boolean.name": "Name",
"form.attribute.item.string.name": "Name",
"form.attribute.item.enumeration.name": "Name",
"form.attribute.item.enumeration.rules": "Values (separate them with a comma)",
"form.attribute.item.enumeration.placeholder": "Ex: morning,noon,evening",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.appearance.name": "Appearance",
"form.attribute.item.appearance.label": "Display as a WYSIWYG",
"form.attribute.item.appearance.description":
@ -113,11 +118,11 @@
"notification.error.message": "发生错误",
"notification.info.contentType.creating.notSaved":
"在创建新Content Type之前请保存当前Content Type",
"notification.info.disable": "这个字段暂时不可编辑...😮",
"notification.info.optimized": "这个插件是用本地存储优化的",
"notification.success.message.contentType.edit": "你的Content Type已更新",
"notification.success.message.contentType.create": "你的Content Type已创建",
"notification.success.contentTypeDeleted": "这个Content Type已被删除",
"notification.info.enumeration": "这个字段暂时不可编辑...😮",
"popUpForm.attributes.string.description": "标题、名称、段落、名称列表",
"popUpForm.attributes.text.description": "描述、文本段落、文章",
@ -129,7 +134,8 @@
"popUpForm.attributes.relation.description": "引用其它 Content Type",
"popUpForm.attributes.email.description": "用户email...",
"popUpForm.attributes.password.description": "用户密码...",
"popUpForm.attributes.enumeration.description": "List of choices",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.boolean.name": "Boolean",
@ -140,6 +146,7 @@
"popUpForm.attributes.relation.name": "Relation",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "Password",
"popUpForm.attributes.enumeration.name": "Enumeration",
"popUpForm.create": "增加新的",
"popUpForm.edit": "编辑",
"popUpForm.field": "字段",

View File

@ -14,6 +14,7 @@
"attribute.email": "Email",
"attribute.password": "密碼",
"attribute.relation": "關聯其他結構",
"attribute.enumeration": "Enumeration",
"attribute.WYSIWYG": "Text (WYSIWYG)",
"contentType.temporaryDisplay": "(未儲存)",
@ -49,6 +50,11 @@
"form.attribute.item.json.name": "名稱",
"form.attribute.item.boolean.name": "名稱",
"form.attribute.item.string.name": "名稱",
"form.attribute.item.enumeration.name": "名稱",
"form.attribute.item.enumeration.rules": "Values (separate them with a comma)",
"form.attribute.item.enumeration.placeholder": "Ex: morning,noon,evening",
"form.attribute.item.enumeration.graphql": "Name override for GraphQL",
"form.attribute.item.enumeration.graphql.description": "Allows you to override the default generated name for GraphQL",
"form.attribute.item.settings.name": "設定",
"form.attribute.item.requiredField": "必填欄位",
"form.attribute.item.uniqueField": "唯一欄位",
@ -105,6 +111,7 @@
"notification.error.message": "有錯誤發生了",
"notification.info.contentType.creating.notSaved": "建立新的資料結構前,請先儲存現在的。",
"notification.info.disable": "这个字段暂时不可编辑...😮",
"notification.info.optimized": "這個擴充功能使用您的 localstorage 來最佳化",
"notification.success.message.contentType.edit": "您的資料結構已更新",
"notification.success.message.contentType.create": "您的資料結構已建立",
@ -120,7 +127,8 @@
"popUpForm.attributes.relation.description": "關聯到一個資料結構",
"popUpForm.attributes.email.description": "使用者的 email...",
"popUpForm.attributes.password.description": "使用者密碼...",
"popUpForm.attributes.enumeration.description": "List of choices",
"popUpForm.attributes.string.name": "字串",
"popUpForm.attributes.text.name": "文字",
"popUpForm.attributes.boolean.name": "是/否",
@ -131,6 +139,7 @@
"popUpForm.attributes.relation.name": "關聯其他結構",
"popUpForm.attributes.email.name": "Email",
"popUpForm.attributes.password.name": "密碼",
"popUpForm.attributes.enumeration.name": "Enumeration",
"popUpForm.create": "增加新的",
"popUpForm.edit": "編輯",
"popUpForm.field": "欄位",

View File

@ -25,7 +25,7 @@
"glob": "^7.1.2",
"graphql": "^0.13.2",
"graphql-depth-limit": "^1.1.0",
"graphql-playground-middleware-koa": "^1.5.1",
"graphql-playground-middleware-koa": "^1.6.1",
"graphql-tools": "^2.23.1",
"graphql-type-json": "^0.2.0",
"pluralize": "^7.0.0",
@ -48,4 +48,4 @@
"npm": ">= 5.3.0"
},
"license": "MIT"
}
}

View File

@ -3,7 +3,8 @@ module.exports = {
actions: {
create: 'User.create', // Use the User plugin's controller.
update: 'User.update',
destroy: 'User.destroy'
destroy: 'User.destroy',
deleteall: 'User.destroyAll',
},
attributes: {
username: {

View File

@ -153,6 +153,13 @@ module.exports = {
destroy: async (ctx) => {
const data = await strapi.plugins['users-permissions'].services.user.remove(ctx.params);
// Send 200 `ok`
ctx.send(data);
},
destroyAll: async (ctx) => {
const data = await strapi.plugins['users-permissions'].services.user.removeAll(ctx.params, ctx.request.query);
// Send 200 `ok`
ctx.send(data);
}

View File

@ -40,4 +40,4 @@
"configurable": false
}
}
}
}

View File

@ -70,13 +70,13 @@ exports.connect = (provider, query) => {
return resolve([null, [{ messages: [{ id: 'Auth.form.error.email.taken' }] }], 'Email is already taken.']);
}
// Retrieve role `public`.
const publicRole = await strapi.query('role', 'users-permissions').findOne({ type: 'public' }, []);
// Retrieve role `authenticated`.
const authenticatedRole = await strapi.query('role', 'users-permissions').findOne({ type: 'authenticated' }, []);
// Create the new user.
const params = _.assign(profile, {
provider: provider,
role: publicRole._id || publicRole.id
role: authenticatedRole._id || authenticatedRole.id
});
const createdUser = await strapi.query('user', 'users-permissions').create(params);

View File

@ -115,6 +115,30 @@ module.exports = {
return strapi.query('user', 'users-permissions').delete(params);
},
removeAll: async (params, query) => {
// Use Content Manager business logic to handle relation.
if (strapi.plugins['content-manager']) {
params.model = 'user';
query.source = 'users-permissions';
return await strapi.plugins['content-manager'].services['contentmanager'].deleteMany(params, query);
}
// TODO remove this logic when we develop plugins' dependencies
const primaryKey = strapi.query('user', 'users-permissions').primaryKey;
const toRemove = Object.keys(query).reduce((acc, curr) => {
if (curr !== 'source') {
return acc.concat([query[curr]]);
}
return acc;
}, []);
return strapi.query('user', 'users-permissions').deleteMany({
[primaryKey]: toRemove,
});
},
validatePassword: (password, hash) => {
return bcrypt.compareSync(password, hash);
}

View File

@ -474,7 +474,7 @@ module.exports = {
let type;
if (_.includes(['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss'], _.last(suffix))) {
if (_.includes(['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss', 'in'], _.last(suffix))) {
type = `_${_.last(suffix)}`;
key = _.dropRight(suffix).join('_');
} else {