mirror of
https://github.com/strapi/strapi.git
synced 2025-10-17 11:08:14 +00:00
Merge branch 'front-media-lib-search' of github.com:strapi/strapi into front/media-lib-filters
Signed-off-by: Virginie Ky <virginie.ky@gmail.com>
This commit is contained in:
commit
45c8deac4d
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -9,29 +9,3 @@ To help us merge your PR, make sure to follow the instructions below:
|
||||
-->
|
||||
|
||||
#### Description of what you did:
|
||||
|
||||
<!--
|
||||
Replace [ ] by [x] to check these checkboxes!
|
||||
-->
|
||||
|
||||
#### My PR is a:
|
||||
|
||||
- [ ] 💥 Breaking change
|
||||
- [ ] 🐛 Bug fix
|
||||
- [ ] 💅 Enhancement
|
||||
- [ ] 🚀 New feature
|
||||
|
||||
#### Main update on the:
|
||||
|
||||
- [ ] Admin
|
||||
- [ ] Documentation
|
||||
- [ ] Framework
|
||||
- [ ] Plugin
|
||||
|
||||
#### Manual testing done on the following databases:
|
||||
|
||||
- [ ] Not applicable
|
||||
- [ ] MongoDB
|
||||
- [ ] MySQL
|
||||
- [ ] Postgres
|
||||
- [ ] SQLite
|
||||
|
@ -205,6 +205,7 @@ module.exports = {
|
||||
'/3.0.0-beta.x/guides/custom-data-response',
|
||||
'/3.0.0-beta.x/guides/custom-admin',
|
||||
'/3.0.0-beta.x/guides/client',
|
||||
'/3.0.0-beta.x/guides/is-owner',
|
||||
'/3.0.0-beta.x/guides/draft',
|
||||
'/3.0.0-beta.x/guides/scheduled-publication',
|
||||
'/3.0.0-beta.x/guides/slug',
|
||||
|
132
docs/3.0.0-beta.x/guides/is-owner.md
Normal file
132
docs/3.0.0-beta.x/guides/is-owner.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Create is owner policy
|
||||
|
||||
This guide will explain how to restrict content edition to content authors only.
|
||||
|
||||
## Introduction
|
||||
|
||||
It is often required that the author of an entry is the only user allowed to edit or delete the entry.
|
||||
|
||||
This is a feature that is requested a lot and in this guide we will see how to implement it.
|
||||
|
||||
## Example
|
||||
|
||||
For this example, we will need an Article Content Type.
|
||||
|
||||
Add a `text` field and a `relation` field for this Content Type.
|
||||
|
||||
The `relation` field is a **many-to-one** relation with User.<br>
|
||||
One User can have many Articles and one Article can have only one User.<br>
|
||||
Name the field `author` for the Article Content Type and `articles` on the User side.
|
||||
|
||||
Now we are ready to start customization.
|
||||
|
||||
## Apply the author by default
|
||||
|
||||
When we are creating a new Article via `POST /articles` we will need to set the authenticated user as the author of the article.
|
||||
|
||||
To do so we will customize the `create` controller function of the Article API.
|
||||
|
||||
**Concepts we will use:**
|
||||
Here is the code of [core controllers](../concepts/controllers.html#core-controllers).
|
||||
We will also use this [documentation](../plugins/users-permissions.html#user-object-in-strapi-context) to access the current authenticated user information.
|
||||
|
||||
**Path —** `./api/article/controllers/Article.js`
|
||||
|
||||
```js
|
||||
const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Create a record.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
async create(ctx) {
|
||||
let entity;
|
||||
if (ctx.is('multipart')) {
|
||||
const { data, files } = parseMultipartData(ctx);
|
||||
data.author = ctx.state.user.id;
|
||||
entity = await strapi.services.article.create(data, { files });
|
||||
} else {
|
||||
ctx.request.body.author = ctx.state.user.id;
|
||||
entity = await strapi.services.article.create(ctx.request.body);
|
||||
}
|
||||
return sanitizeEntity(entity, { model: strapi.models.article });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Now, when an article is created, the authenticated user is automaticaly set as author of the article.
|
||||
|
||||
## Limit the update
|
||||
|
||||
Now we will restrict the update of articles only for the author.
|
||||
|
||||
We will use the same concepts as previously.
|
||||
|
||||
**Path —** `./api/article/controllers/Article.js`
|
||||
|
||||
```js
|
||||
const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Create a record.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
async create(ctx) {
|
||||
let entity;
|
||||
if (ctx.is('multipart')) {
|
||||
const { data, files } = parseMultipartData(ctx);
|
||||
data.author = ctx.state.user.id;
|
||||
entity = await strapi.services.article.create(data, { files });
|
||||
} else {
|
||||
ctx.request.body.author = ctx.state.user.id;
|
||||
entity = await strapi.services.article.create(ctx.request.body);
|
||||
}
|
||||
return sanitizeEntity(entity, { model: strapi.models.article });
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a record.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
async update(ctx) {
|
||||
let entity;
|
||||
|
||||
const [article] = await strapi.services.article.find({
|
||||
id: ctx.params.id,
|
||||
'author.id': ctx.state.user.id,
|
||||
});
|
||||
|
||||
if (!article) {
|
||||
return ctx.unauthorized(`You can't update this entry`);
|
||||
}
|
||||
|
||||
if (ctx.is('multipart')) {
|
||||
const { data, files } = parseMultipartData(ctx);
|
||||
entity = await strapi.services.article.update(ctx.params, data, {
|
||||
files,
|
||||
});
|
||||
} else {
|
||||
entity = await strapi.services.article.update(
|
||||
ctx.params,
|
||||
ctx.request.body
|
||||
);
|
||||
}
|
||||
|
||||
return sanitizeEntity(entity, { model: strapi.models.article });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
And tada!
|
||||
|
||||
::: tip
|
||||
For the delete action, it will be the exact same check than the update action.
|
||||
:::
|
@ -191,3 +191,85 @@ export default strapi => {
|
||||
return strapi.registerPlugin(plugin);
|
||||
};
|
||||
```
|
||||
|
||||
## Adding a setting into the global section
|
||||
|
||||
In order to add a link into the global section of the settings view you need to create a global array containing the links you want to add;
|
||||
|
||||
**Path —** `plugins/my-plugin/admin/src/index.js`.
|
||||
|
||||
```
|
||||
import pluginPkg from '../../package.json';
|
||||
// Import the component
|
||||
import Settings from './containers/Settings';
|
||||
import SettingLink from './components/SettingLink';
|
||||
import pluginId from './pluginId';
|
||||
|
||||
export default strapi => {
|
||||
const pluginDescription =
|
||||
pluginPkg.strapi.description || pluginPkg.description;
|
||||
|
||||
// Declare the links that will be injected into the settings menu
|
||||
const menuSection = {
|
||||
id: pluginId,
|
||||
title: {
|
||||
id: `${pluginId}.foo`,
|
||||
defaultMessage: 'Super cool setting',
|
||||
},
|
||||
links: [
|
||||
{
|
||||
title: 'Setting page 1',
|
||||
to: `${strapi.settingsBaseURL}/${pluginId}/setting1`,
|
||||
name: 'setting1',
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: `${pluginId}.bar`,
|
||||
defaultMessage: 'Setting page 2',
|
||||
},
|
||||
to: `${strapi.settingsBaseURL}/${pluginId}/setting2`,
|
||||
name: 'setting2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
blockerComponent: null,
|
||||
blockerComponentProps: {},
|
||||
description: pluginDescription,
|
||||
icon: pluginPkg.strapi.icon,
|
||||
id: pluginId,
|
||||
initializer: () => null,
|
||||
injectedComponents: [],
|
||||
isReady: true,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: null,
|
||||
name: pluginPkg.strapi.name,
|
||||
preventComponentRendering: false,
|
||||
settings: {
|
||||
// Add a link into the global section of the settings view
|
||||
global: [
|
||||
{
|
||||
title: 'Setting link 1',
|
||||
to: `${strapi.settingsBaseURL}/setting-link-1`,
|
||||
name: 'settingLink1',
|
||||
Component: SettingLink,
|
||||
// Bool : https://reacttraining.com/react-router/web/api/Route/exact-bool
|
||||
exact: false,
|
||||
},
|
||||
],
|
||||
mainComponent: Settings,
|
||||
menuSection,
|
||||
},
|
||||
trads: {},
|
||||
};
|
||||
|
||||
return strapi.registerPlugin(plugin);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
::: danger
|
||||
It is currently not possible to add a link into another plugin's setting section
|
||||
:::
|
||||
|
@ -5,7 +5,7 @@ import Logo from '../../assets/images/logo-strapi.png';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: #007eff;
|
||||
height: ${props => props.theme.main.sizes.header.height};
|
||||
height: ${props => props.theme.main.sizes.leftMenu.height};
|
||||
|
||||
.leftMenuHeaderLink {
|
||||
&:hover {
|
||||
@ -18,7 +18,7 @@ const Wrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: ${props => props.theme.main.sizes.header.height};
|
||||
height: ${props => props.theme.main.sizes.leftMenu.height};
|
||||
vertical-align: middle;
|
||||
font-size: 2rem;
|
||||
letter-spacing: 0.2rem;
|
||||
|
@ -4,12 +4,14 @@ import PropTypes from 'prop-types';
|
||||
const Wrapper = styled.div`
|
||||
padding-top: 0.7rem;
|
||||
position: absolute;
|
||||
top: 6rem;
|
||||
top: ${props => props.theme.main.sizes.leftMenu.height};
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - (6rem + 10.2rem));
|
||||
height: calc(
|
||||
100vh - (${props => props.theme.main.sizes.leftMenu.height} + 10.2rem)
|
||||
);
|
||||
box-sizing: border-box;
|
||||
|
||||
.title {
|
||||
|
@ -6,29 +6,30 @@
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { useGlobalContext, LeftMenu, LeftMenuList } from 'strapi-helper-plugin';
|
||||
import { get } from 'lodash';
|
||||
import { Switch, Redirect, Route, useParams } from 'react-router-dom';
|
||||
|
||||
import EditView from '../Webhooks/EditView';
|
||||
import ListView from '../Webhooks/ListView';
|
||||
import SettingDispatcher from './SettingDispatcher';
|
||||
import Wrapper from './Wrapper';
|
||||
import retrieveGlobalLinks from './utils/retrieveGlobalLinks';
|
||||
import retrievePluginsMenu from './utils/retrievePluginsMenu';
|
||||
|
||||
function SettingsPage() {
|
||||
const { settingId } = useParams();
|
||||
const { formatMessage, plugins, settingsBaseURL } = useGlobalContext();
|
||||
// Retrieve the links that will be injected into the global section
|
||||
const globalLinks = retrieveGlobalLinks(plugins);
|
||||
// Create the plugins settings section
|
||||
// Note it is currently not possible to add a link into a plugin section
|
||||
const pluginsMenu = retrievePluginsMenu(plugins);
|
||||
|
||||
const pluginsMenu = Object.keys(plugins).reduce((acc, current) => {
|
||||
const pluginMenu = get(plugins, [current, 'settings', 'menuSection'], null);
|
||||
|
||||
if (!pluginMenu) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push(pluginMenu);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
const createdRoutes = globalLinks
|
||||
.map(({ to, Component, exact }) => (
|
||||
<Route path={to} key={to} component={Component} exact={exact || false} />
|
||||
))
|
||||
.filter((route, index, refArray) => {
|
||||
return refArray.findIndex(obj => obj.key === route.key) === index;
|
||||
});
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
@ -40,6 +41,7 @@ function SettingsPage() {
|
||||
to: `${settingsBaseURL}/webhooks`,
|
||||
name: 'webhooks',
|
||||
},
|
||||
...globalLinks,
|
||||
],
|
||||
},
|
||||
...pluginsMenu,
|
||||
@ -74,6 +76,7 @@ function SettingsPage() {
|
||||
path={`${settingsBaseURL}/webhooks/:id`}
|
||||
component={EditView}
|
||||
/>
|
||||
{createdRoutes}
|
||||
<Route
|
||||
path={`${settingsBaseURL}/:pluginId`}
|
||||
component={SettingDispatcher}
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { get } from 'lodash';
|
||||
|
||||
const retrieveGlobalLinks = pluginsObj => {
|
||||
return Object.values(pluginsObj).reduce((acc, current) => {
|
||||
const links = get(current, ['settings', 'global'], null);
|
||||
|
||||
if (links) {
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
acc.push(links[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default retrieveGlobalLinks;
|
@ -0,0 +1,17 @@
|
||||
import { get } from 'lodash';
|
||||
|
||||
const retrievePluginsMenu = pluginsObj => {
|
||||
return Object.values(pluginsObj).reduce((acc, current) => {
|
||||
const pluginMenu = get(current, ['settings', 'menuSection'], null);
|
||||
|
||||
if (!pluginMenu) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push(pluginMenu);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default retrievePluginsMenu;
|
@ -0,0 +1,33 @@
|
||||
import retrieveGlobalLinks from '../retrieveGlobalLinks';
|
||||
|
||||
describe('ADMIN | containers | SettingsPage | utils', () => {
|
||||
describe('retrieveGlobalLinks', () => {
|
||||
it('should return an empty array if there is no plugins', () => {
|
||||
expect(retrieveGlobalLinks({})).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return an array of links', () => {
|
||||
const plugins = {
|
||||
test: {
|
||||
settings: {
|
||||
global: [],
|
||||
},
|
||||
},
|
||||
noSettings: {},
|
||||
foo: {
|
||||
settings: {
|
||||
global: ['test'],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
settings: {
|
||||
global: ['test2'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = ['test', 'test2'];
|
||||
|
||||
expect(retrieveGlobalLinks(plugins)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
import retrievePluginsMenu from '../retrievePluginsMenu';
|
||||
|
||||
describe('ADMIN | containers | SettingsPage | utils', () => {
|
||||
describe('retrievePluginsMenu', () => {
|
||||
it('should return an empty array if there is no plugins', () => {
|
||||
expect(retrievePluginsMenu({})).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return an array of menu sections', () => {
|
||||
const plugins = {
|
||||
test: {
|
||||
settings: {
|
||||
menuSection: null,
|
||||
},
|
||||
},
|
||||
noSettings: {},
|
||||
foo: {
|
||||
settings: {
|
||||
menuSection: { label: 'test' },
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
settings: {
|
||||
menuSection: { label: 'test2' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [{ label: 'test' }, { label: 'test2' }];
|
||||
|
||||
expect(retrievePluginsMenu(plugins)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
@ -15,9 +15,16 @@ const colors = {
|
||||
'gray-lighter': '#eceeef',
|
||||
'gray-lightest': '#f7f7f9',
|
||||
brightGrey: '#f0f3f8',
|
||||
darkGrey: '#e3e9f3',
|
||||
lightGrey: '#fafafa',
|
||||
lightestGrey: '#fbfbfb',
|
||||
mediumGrey: '#F2F3F4',
|
||||
grey: '#9ea7b8',
|
||||
greyDark: '#292b2c',
|
||||
greyAlpha: 'rgba(227, 233, 243, 0.5)',
|
||||
lightBlue: '#E6F0FB',
|
||||
mediumBlue: '#007EFF',
|
||||
darkBlue: '#AED4FB',
|
||||
|
||||
content: {
|
||||
background: '#fafafb',
|
||||
@ -35,27 +42,6 @@ const colors = {
|
||||
'blue-dark': '#151c2e',
|
||||
blue: '#0097f7',
|
||||
},
|
||||
filters: {
|
||||
border: '#e3e9f3',
|
||||
background: '#ffffff',
|
||||
color: '#292b2c',
|
||||
shadow: 'rgba(227, 233, 243, 0.5)',
|
||||
button: {
|
||||
hover: {
|
||||
background: '#F7F8F8',
|
||||
},
|
||||
active: {
|
||||
background: '#E6F0FB',
|
||||
border: '#AED4FB',
|
||||
color: '#007EFF',
|
||||
},
|
||||
},
|
||||
list: {
|
||||
hover: {
|
||||
background: '#f6f6f6',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default colors;
|
||||
|
@ -22,6 +22,7 @@
|
||||
"app.components.BlockLink.documentation.content": "Ознакомьтесь с концепциями, справочниками и обучающими материалами.",
|
||||
"app.components.Button.cancel": "Отменить",
|
||||
"app.components.Button.save": "Сохранить",
|
||||
"app.components.Button.reset": "Сброс",
|
||||
"app.components.ComingSoonPage.comingSoon": "Скоро",
|
||||
"app.components.ComingSoonPage.featuresNotAvailable": "Этот функционал все еще находится в активной разработке.",
|
||||
"app.components.DownloadInfo.download": "Выполняется загрузка...",
|
||||
@ -75,8 +76,10 @@
|
||||
"app.components.LeftMenuLinkContainer.general": "Общие",
|
||||
"app.components.LeftMenuLinkContainer.installNewPlugin": "Магазин",
|
||||
"app.components.LeftMenuLinkContainer.listPlugins": "Плагины",
|
||||
"app.components.LeftMenuLinkContainer.contentTypes": "Типы контента",
|
||||
"app.components.LeftMenuLinkContainer.noPluginsInstalled": "Нет установленных плагинов",
|
||||
"app.components.LeftMenuLinkContainer.plugins": "Плагины",
|
||||
"app.components.LeftMenuLinkContainer.settings": "Настройки",
|
||||
"app.components.ListPluginsPage.description": "Список установленных плагинов в проекте.",
|
||||
"app.components.ListPluginsPage.helmet.title": "Список плагинов",
|
||||
"app.components.ListPluginsPage.title": "Плагины",
|
||||
@ -105,6 +108,7 @@
|
||||
"app.components.listPlugins.title.plural": "{number} плагинов установлено",
|
||||
"app.components.listPlugins.title.singular": "{number} плагин установлен",
|
||||
"app.components.listPluginsPage.deletePlugin.error": "Произошла ошибка при удалении плагина",
|
||||
"app.links.configure-view": "Настройка представления",
|
||||
"app.utils.SelectOption.defaultMessage": " ",
|
||||
"app.utils.defaultMessage": " ",
|
||||
"app.utils.placeholder.defaultMessage": " ",
|
||||
@ -125,7 +129,9 @@
|
||||
"components.Input.error.validation.minSupMax": "Не может быть выше",
|
||||
"components.Input.error.validation.regex": "Значение не соответствует регулярному выражению.",
|
||||
"components.Input.error.validation.required": "Обязательное значение.",
|
||||
"components.Input.error.validation.unique": "Это значение уже используется.",
|
||||
"components.InputSelect.option.placeholder": "Выберите здесь",
|
||||
"component.Input.error.validation.integer": "Значение должно быть целочисленным",
|
||||
"components.ListRow.empty": "Нет данных для отображения.",
|
||||
"components.OverlayBlocker.description": "Вы воспользовались функционалом, который требует перезапуска сервера. Пожалуйста, подождите, пока сервер не будет запущен.",
|
||||
"components.OverlayBlocker.description.serverError": "Сервер должен был перезагрузиться, пожалуйста, проверьте ваши логи в терминале.",
|
||||
@ -163,7 +169,6 @@
|
||||
"HomePage.community": "Присоединяйтесь к сообществу",
|
||||
"HomePage.roadmap": "Смотрите нашу дорожную карту",
|
||||
"HomePage.greetings": "Привет {name}!",
|
||||
|
||||
"Auth.advanced.allow_register": "",
|
||||
"Auth.privacy-policy-agreement.terms": "условия",
|
||||
"Auth.privacy-policy-agreement.policy": "политика конфиденциальности",
|
||||
@ -212,8 +217,43 @@
|
||||
"Auth.header.register.description": "Для завершения установки и обеспечения безопасности приложения, создайте вашего первого пользователя (root admin), заполнив форму ниже.",
|
||||
"Auth.link.forgot-password": "Забыли пароль?",
|
||||
"Auth.link.ready": "Готовы войти?",
|
||||
"Settings.global": "Глобальные настройки",
|
||||
"Settings.error": "Ошибка",
|
||||
"Settings.webhooks.title": "Webhooks",
|
||||
"Settings.webhooks.singular": "webhook",
|
||||
"Settings.webhooks.list.description": "Уведомления с помощью POST событий.",
|
||||
"Settings.webhooks.list.button.add": "Добавить новый webhook",
|
||||
"Settings.webhooks.list.button.delete": "Удалить",
|
||||
"Settings.webhooks.list.empty.title": "Пока еще нет webhooks",
|
||||
"Settings.webhooks.list.empty.description": "Добавить первый в этот список.",
|
||||
"Settings.webhooks.list.empty.link": "Просмотреть документацию",
|
||||
"Settings.webhooks.enabled": "Включен",
|
||||
"Settings.webhooks.disabled": "Выключен",
|
||||
"Settings.webhooks.create": "Создание webhook",
|
||||
"Settings.webhooks.create.header": "Создание нового заголовка",
|
||||
"Settings.webhooks.form.name": "Название",
|
||||
"Settings.webhooks.form.url": "Url",
|
||||
"Settings.webhooks.form.headers": "Заголовки",
|
||||
"Settings.webhooks.form.events": "События",
|
||||
"Settings.webhooks.key": "Ключ",
|
||||
"Settings.webhooks.value": "Значение",
|
||||
"Settings.webhooks.trigger": "Триггер",
|
||||
"Settings.webhooks.trigger.title": "Сохранить перед триггером",
|
||||
"Settings.webhooks.trigger.cancel": "Отмена триггера",
|
||||
"Settings.webhooks.trigger.pending": "Ожидание…",
|
||||
"Settings.webhooks.trigger.success": "Успех!",
|
||||
"Settings.webhooks.trigger.success.label": "Триггер выполнен",
|
||||
"Settings.webhooks.trigger.save": "Пожалуйста сохраните триггер",
|
||||
"Settings.webhooks.trigger.test": "Тест триггер",
|
||||
"Settings.webhooks.events.create": "Создание",
|
||||
"Settings.webhooks.events.edit": "Редактирование",
|
||||
"Settings.webhooks.events.delete": "Удаление",
|
||||
"Settings.webhooks.created": "Webhook создан",
|
||||
"app.containers.App.notification.error.init": "Произошла ошибка при запросе к API",
|
||||
"components.Input.error.password.noMatch": "Пароль не совпадает",
|
||||
"form.button.done": "Выполнено",
|
||||
"notification.form.error.fields": "Форма содержит некоторые ошибки"
|
||||
"form.button.finish": "Финиш",
|
||||
"notification.form.error.fields": "Форма содержит некоторые ошибки",
|
||||
"notification.form.success.fields": "Изменения сохранены",
|
||||
"global.prompt.unsaved": "Вы действительно хотите покинуть эту страницу? Все Ваши изменения будут потеряны"
|
||||
}
|
||||
|
@ -249,6 +249,7 @@ async function watchFiles(dir, ignoreFiles = []) {
|
||||
const cacheDir = path.join(dir, '.cache');
|
||||
const pkgJSON = require(path.join(dir, 'package.json'));
|
||||
const admin = path.join(dir, 'admin');
|
||||
const extensionsPath = path.join(dir, 'extensions');
|
||||
|
||||
const appPlugins = Object.keys(pkgJSON.dependencies).filter(
|
||||
dep =>
|
||||
@ -256,12 +257,7 @@ async function watchFiles(dir, ignoreFiles = []) {
|
||||
fs.existsSync(path.resolve(getPkgPath(dep), 'admin', 'src', 'index.js'))
|
||||
);
|
||||
const pluginsToWatch = appPlugins.map(plugin =>
|
||||
path.join(
|
||||
dir,
|
||||
'extensions',
|
||||
plugin.replace(/^strapi-plugin-/i, ''),
|
||||
'admin'
|
||||
)
|
||||
path.join(extensionsPath, plugin.replace(/^strapi-plugin-/i, ''), 'admin')
|
||||
);
|
||||
const filesToWatch = [admin, ...pluginsToWatch];
|
||||
|
||||
@ -272,20 +268,20 @@ async function watchFiles(dir, ignoreFiles = []) {
|
||||
});
|
||||
|
||||
watcher.on('all', async (event, filePath) => {
|
||||
const re = /\/extensions\/([^\/]*)\/.*$/gm;
|
||||
const matched = re.exec(filePath);
|
||||
const isExtension = matched !== null;
|
||||
const pluginName = isExtension ? matched[1] : '';
|
||||
const isExtension = filePath.includes(extensionsPath);
|
||||
const pluginName = isExtension
|
||||
? filePath.replace(extensionsPath, '').split(path.sep)[1]
|
||||
: '';
|
||||
|
||||
const packageName = isExtension
|
||||
? `strapi-plugin-${pluginName}`
|
||||
: 'strapi-admin';
|
||||
|
||||
const targetPath = isExtension
|
||||
? filePath
|
||||
.split(`${path.sep}extensions${path.sep}`)[1]
|
||||
.replace(pluginName, '')
|
||||
: filePath.split(`${path.sep}admin`)[1];
|
||||
? path.normalize(
|
||||
filePath.split(extensionsPath)[1].replace(pluginName, '')
|
||||
)
|
||||
: path.normalize(filePath.split(admin)[1]);
|
||||
|
||||
const destFolder = isExtension
|
||||
? path.join(cacheDir, 'plugins', packageName)
|
||||
|
@ -6,7 +6,14 @@ import SearchInfo from '../SearchInfo';
|
||||
import Clear from './Clear';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const HeaderSearch = ({ label, onChange, onClear, placeholder, value }) => {
|
||||
const HeaderSearch = ({
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
onClear,
|
||||
placeholder,
|
||||
value,
|
||||
}) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<div>
|
||||
@ -14,6 +21,7 @@ const HeaderSearch = ({ label, onChange, onClear, placeholder, value }) => {
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
type="text"
|
||||
@ -32,6 +40,7 @@ const HeaderSearch = ({ label, onChange, onClear, placeholder, value }) => {
|
||||
|
||||
HeaderSearch.defaultProps = {
|
||||
label: '',
|
||||
name: '',
|
||||
onChange: () => {},
|
||||
onClear: () => {},
|
||||
placeholder: 'Search for an entry',
|
||||
@ -40,6 +49,7 @@ HeaderSearch.defaultProps = {
|
||||
|
||||
HeaderSearch.propTypes = {
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
|
@ -87,6 +87,9 @@ export {
|
||||
useGlobalContext,
|
||||
} from './contexts/GlobalContext';
|
||||
|
||||
// Hooks
|
||||
export { default as useQuery } from './hooks/useQuery';
|
||||
|
||||
// Utils
|
||||
export { default as auth } from './utils/auth';
|
||||
export { default as cleanData } from './utils/cleanData';
|
||||
|
@ -14,10 +14,9 @@ import { AttributeIcon } from '@buffetjs/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useGlobalContext } from 'strapi-helper-plugin';
|
||||
import { useGlobalContext, useQuery } from 'strapi-helper-plugin';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import makeSearch from '../../utils/makeSearch';
|
||||
import useQuery from '../../hooks/useQuery';
|
||||
import Button from './Button';
|
||||
import Card from './Card';
|
||||
|
||||
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { components } from 'react-select';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { useQuery } from 'strapi-helper-plugin';
|
||||
import useDataManager from '../../hooks/useDataManager';
|
||||
import useQuery from '../../hooks/useQuery';
|
||||
import Category from './Category';
|
||||
import Ul from './Ul';
|
||||
|
||||
|
@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
|
||||
import { components } from 'react-select';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { get } from 'lodash';
|
||||
import { useQuery } from 'strapi-helper-plugin';
|
||||
import { Checkbox, CheckboxWrapper, Label } from '@buffetjs/styles';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import useDataManager from '../../hooks/useDataManager';
|
||||
import useQuery from '../../hooks/useQuery';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import UpperFirst from '../UpperFirst';
|
||||
import SubUl from './SubUl';
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
ModalForm,
|
||||
getYupInnerErrors,
|
||||
useGlobalContext,
|
||||
useQuery,
|
||||
InputsIndex,
|
||||
} from 'strapi-helper-plugin';
|
||||
import { Button } from '@buffetjs/core';
|
||||
@ -16,7 +17,6 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { get, has, isEmpty, set, toLower, toString, upperFirst } from 'lodash';
|
||||
import pluginId from '../../pluginId';
|
||||
import useQuery from '../../hooks/useQuery';
|
||||
import useDataManager from '../../hooks/useDataManager';
|
||||
import AttributeOption from '../../components/AttributeOption';
|
||||
import BooleanBox from '../../components/BooleanBox';
|
||||
|
@ -0,0 +1,29 @@
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { themePropTypes } from 'strapi-helper-plugin';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 5px;
|
||||
background-color: ${({ theme }) => theme.main.colors.white};
|
||||
border: 1px solid #e3e9f3;
|
||||
border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
color: ${({ color }) => color};
|
||||
`;
|
||||
|
||||
Wrapper.defaultProps = {
|
||||
color: '#b3b5b9',
|
||||
};
|
||||
|
||||
Wrapper.propTypes = {
|
||||
color: PropTypes.string,
|
||||
...themePropTypes,
|
||||
};
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Pencil } from '@buffetjs/icons';
|
||||
import { ClearIcon } from 'strapi-helper-plugin';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const CardControl = ({ color, onClick, type }) => {
|
||||
return (
|
||||
<Wrapper onClick={onClick} color={color}>
|
||||
{type === 'pencil' && <Pencil fill={color} />}
|
||||
{type === 'clear' && <ClearIcon fill={color} />}
|
||||
{!['pencil', 'clear'].includes(type) && <FontAwesomeIcon icon={type} />}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
CardControl.defaultProps = {
|
||||
color: '#b3b5b9',
|
||||
onClick: () => {},
|
||||
type: 'pencil',
|
||||
};
|
||||
|
||||
CardControl.propTypes = {
|
||||
color: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CardControl;
|
@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const CardControlsWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 5px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export default CardControlsWrapper;
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
const CardImgWrapper = styled.div`
|
||||
position: relative;
|
||||
height: ${({ isSmall }) => (isSmall ? '127px' : '156px')};
|
||||
min-width: ${({ isSmall }) => (isSmall ? '200px' : '245px')};
|
||||
min-width: ${({ isSmall }) => (isSmall ? '100%' : '245px')};
|
||||
border-radius: 2px;
|
||||
background: ${({ withOverlay }) => (withOverlay ? '#F6F6F6' : '#333740')};
|
||||
|
||||
@ -18,6 +18,17 @@ const CardImgWrapper = styled.div`
|
||||
|
||||
return '';
|
||||
}}
|
||||
|
||||
.card-control-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.card-control-wrapper {
|
||||
display: flex;
|
||||
z-index: 1050;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
CardImgWrapper.defaultProps = {
|
||||
|
@ -8,9 +8,9 @@ const DropdownButton = styled.button`
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
line-height: 30px;
|
||||
background-color: ${({ theme }) => theme.main.colors.filters.background};
|
||||
border: 1px solid ${({ theme }) => theme.main.colors.filters.border};
|
||||
color: ${({ theme }) => theme.main.colors.filters.color};
|
||||
background-color: ${({ theme }) => theme.main.colors.white};
|
||||
border: 1px solid ${({ theme }) => theme.main.colors.darkGrey};
|
||||
color: ${({ theme }) => theme.main.colors.greyDark};
|
||||
font-weight: ${({ theme }) => theme.main.fontWeights.semiBold};
|
||||
font-size: ${({ theme }) => theme.main.fontSizes.md};
|
||||
border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
|
||||
@ -24,24 +24,24 @@ const DropdownButton = styled.button`
|
||||
margin-left: 10px;
|
||||
}
|
||||
> svg g {
|
||||
stroke: ${({ theme }) => theme.main.colors.filters.color};
|
||||
stroke: ${({ theme }) => theme.main.colors.greyDark};
|
||||
}
|
||||
|
||||
${({ isActive, theme }) =>
|
||||
isActive
|
||||
? `
|
||||
background-color: ${theme.main.colors.filters.button.active.background};
|
||||
border: 1px solid ${theme.main.colors.filters.button.active.border};
|
||||
color: ${theme.main.colors.filters.button.active.color};
|
||||
> svg g {
|
||||
stroke: ${theme.main.colors.filters.button.active.color};
|
||||
}
|
||||
`
|
||||
background-color: ${theme.main.colors.lightBlue};
|
||||
border: 1px solid ${theme.main.colors.darkBlue};
|
||||
color: ${theme.main.colors.mediumBlue};
|
||||
> svg g {
|
||||
stroke: ${theme.main.colors.mediumBlue};
|
||||
}
|
||||
`
|
||||
: `
|
||||
&:hover {
|
||||
background-color: ${theme.main.colors.filters.button.hover.background};
|
||||
}
|
||||
`}
|
||||
&:hover {
|
||||
background-color: ${theme.main.colors.lightestGrey};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
DropdownButton.defaultProps = {
|
||||
|
@ -8,9 +8,9 @@ const DropdownSection = styled.div`
|
||||
top: 38px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-color: ${({ theme }) => theme.main.colors.filters.background};
|
||||
border: 1px solid ${({ theme }) => theme.main.colors.filters.border};
|
||||
box-shadow: 0 2px 4px ${({ theme }) => theme.main.colors.filters.shadow};
|
||||
background-color: ${({ theme }) => theme.main.colors.white};
|
||||
border: 1px solid ${({ theme }) => theme.main.colors.darkGrey};
|
||||
box-shadow: 0 2px 4px ${({ theme }) => theme.main.colors.greyAlpha};
|
||||
${({ isOpen }) => isOpen && 'display: block;'}
|
||||
`;
|
||||
|
||||
|
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import Flex from '../Flex';
|
||||
import Text from '../Text';
|
||||
import FileDetailsBoxWrapper from './FileDetailsBoxWrapper';
|
||||
import formatBytes from './utils/formatBytes';
|
||||
|
||||
const FileDetailsBox = ({ file }) => {
|
||||
const sections = [
|
||||
{
|
||||
key: 0,
|
||||
rows: [
|
||||
{ label: 'size', value: formatBytes(get(file, 'size', 0), 0) },
|
||||
{ label: 'date', value: '-' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
type: 'spacer',
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
rows: [
|
||||
{ label: 'dimensions', value: '-' },
|
||||
{ label: 'extension', value: '-' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<FileDetailsBoxWrapper>
|
||||
{sections.map(({ key, rows, type }) => {
|
||||
if (type === 'spacer') {
|
||||
return (
|
||||
<Text as="section" key={key}>
|
||||
<Text> </Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex justifyContent="space-between" key={key}>
|
||||
{rows.map(rowItem => {
|
||||
return (
|
||||
<Text as="div" key={rowItem.label} style={{ width: '50%' }}>
|
||||
<Text
|
||||
color="grey"
|
||||
fontWeight="bold"
|
||||
textTransform="capitalize"
|
||||
>
|
||||
{rowItem.label}
|
||||
</Text>
|
||||
<Text color="grey">{rowItem.value}</Text>
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</FileDetailsBoxWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
FileDetailsBox.defaultProps = {
|
||||
file: {
|
||||
size: 0,
|
||||
},
|
||||
};
|
||||
|
||||
FileDetailsBox.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
size: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
|
||||
export default FileDetailsBox;
|
@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
import { themePropTypes } from 'strapi-helper-plugin';
|
||||
|
||||
const FileDetailsBoxWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 119px;
|
||||
padding: 16px;
|
||||
background-color: ${({ theme }) => theme.main.colors.lightGrey};
|
||||
border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
|
||||
`;
|
||||
|
||||
FileDetailsBoxWrapper.propTypes = themePropTypes;
|
||||
|
||||
export default FileDetailsBoxWrapper;
|
@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
import CardImgWrapper from '../CardImgWrapper';
|
||||
|
||||
const FileWrapper = styled(CardImgWrapper)`
|
||||
height: 401px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
> img {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline-color: ${({ theme }) => theme.main.colors.white};
|
||||
}
|
||||
.cropper-point {
|
||||
background-color: ${({ theme }) => theme.main.colors.white};
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.point-se {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default FileWrapper;
|
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
margin-top: 22px;
|
||||
`;
|
||||
|
||||
export default FormWrapper;
|
@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
import { Row as Base } from 'reactstrap';
|
||||
|
||||
const Row = styled(Base)`
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
export default Row;
|
@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
import ContainerFluid from '../ContainerFluid';
|
||||
|
||||
const Wrapper = styled(ContainerFluid)`
|
||||
padding-top: 18px;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Inputs } from '@buffetjs/custom';
|
||||
import { useGlobalContext } from 'strapi-helper-plugin';
|
||||
import Cropper from 'cropperjs';
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
import CardControl from '../CardControl';
|
||||
import CardControlsWrapper from '../CardControlsWrapper';
|
||||
import ModalSection from '../ModalSection';
|
||||
import Text from '../Text';
|
||||
import FileDetailsBox from './FileDetailsBox';
|
||||
import FileWrapper from './FileWrapper';
|
||||
import FormWrapper from './FormWrapper';
|
||||
import Row from './Row';
|
||||
import Wrapper from './Wrapper';
|
||||
import form from './utils/form';
|
||||
import isImageType from './utils/isImageType';
|
||||
|
||||
const EditForm = ({
|
||||
fileToEdit,
|
||||
onChange,
|
||||
onClickDeleteFileToUpload,
|
||||
onSubmitEditNewFile,
|
||||
setCropResult,
|
||||
}) => {
|
||||
const { formatMessage } = useGlobalContext();
|
||||
const [isCropping, setIsCropping] = useState(false);
|
||||
const [infosCoordinates, setInfosCoordinates] = useState({ top: 0, left: 0 });
|
||||
const [infos, setInfos] = useState({ width: 0, height: 0 });
|
||||
const [src, setSrc] = useState(null);
|
||||
|
||||
const mimeType = get(fileToEdit, ['file', 'type'], '');
|
||||
const isImg = isImageType(mimeType);
|
||||
// TODO
|
||||
const canCrop = isImg && !mimeType.includes('svg');
|
||||
|
||||
const imgRef = useRef();
|
||||
let cropper = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (isImg) {
|
||||
// TODO: update when editing existing file
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
setSrc(reader.result);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(fileToEdit.file);
|
||||
}
|
||||
}, [isImg, fileToEdit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCropping) {
|
||||
cropper.current = new Cropper(imgRef.current, {
|
||||
modal: false,
|
||||
initialAspectRatio: 16 / 9,
|
||||
movable: true,
|
||||
zoomable: false,
|
||||
cropBoxResizable: true,
|
||||
background: false,
|
||||
ready: handleResize,
|
||||
cropmove: handleResize,
|
||||
});
|
||||
} else if (cropper.current) {
|
||||
cropper.current.destroy();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (cropper.current) {
|
||||
cropper.current.destroy();
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cropper, isCropping]);
|
||||
|
||||
const handleResize = () => {
|
||||
const cropBox = cropper.current.getCropBoxData();
|
||||
const { width, height } = cropBox;
|
||||
const roundedWidth = Math.round(width);
|
||||
const roundedHeight = Math.round(height);
|
||||
const xSignWidth = 3;
|
||||
const margin = 15;
|
||||
const pixelWidth = 13;
|
||||
|
||||
const left =
|
||||
cropBox.left +
|
||||
width -
|
||||
margin -
|
||||
((roundedHeight.toString().length +
|
||||
roundedWidth.toString().length +
|
||||
xSignWidth) *
|
||||
pixelWidth) /
|
||||
2;
|
||||
const top = cropBox.top + height - pixelWidth - margin;
|
||||
|
||||
setInfosCoordinates({ top, left });
|
||||
setInfos({ width: roundedWidth, height: roundedHeight });
|
||||
};
|
||||
|
||||
const handleToggleCropMode = () => {
|
||||
setIsCropping(prev => !prev);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (cropper) {
|
||||
const canvas = cropper.current.getCroppedCanvas();
|
||||
canvas.toBlob(blob => {
|
||||
const {
|
||||
file: { lastModifiedDate, lastModified, name },
|
||||
} = fileToEdit;
|
||||
|
||||
setCropResult(
|
||||
new File([blob], name, {
|
||||
type: mimeType,
|
||||
lastModified,
|
||||
lastModifiedDate,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setIsCropping(false);
|
||||
};
|
||||
|
||||
const handleClickDelete = () => {
|
||||
onClickDeleteFileToUpload(fileToEdit.originalIndex);
|
||||
};
|
||||
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
onSubmitEditNewFile(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ModalSection>
|
||||
<Wrapper>
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<FileWrapper>
|
||||
<CardControlsWrapper className="card-control-wrapper">
|
||||
{!isCropping ? (
|
||||
<>
|
||||
<CardControl
|
||||
color="#9EA7B8"
|
||||
type="trash-alt"
|
||||
onClick={handleClickDelete}
|
||||
/>
|
||||
{canCrop && (
|
||||
<CardControl
|
||||
type="crop"
|
||||
color="#9EA7B8"
|
||||
onClick={handleToggleCropMode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CardControl
|
||||
type="clear"
|
||||
color="#F64D0A"
|
||||
onClick={handleToggleCropMode}
|
||||
/>
|
||||
<CardControl
|
||||
type="check"
|
||||
color="#6DBB1A"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardControlsWrapper>
|
||||
|
||||
{isImg ? (
|
||||
<>
|
||||
<img
|
||||
src={src}
|
||||
alt={get(fileToEdit, ['file', 'name'], '')}
|
||||
ref={isCropping ? imgRef : null}
|
||||
/>
|
||||
{isCropping && (
|
||||
<Text
|
||||
fontSize="md"
|
||||
color="white"
|
||||
as="div"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: infosCoordinates.top,
|
||||
left: infosCoordinates.left,
|
||||
background: '#333740',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
|
||||
{infos.width} x {infos.height}
|
||||
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</FileWrapper>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<FileDetailsBox file={fileToEdit.file} />
|
||||
<FormWrapper>
|
||||
{form.map(({ key, inputs }) => {
|
||||
return (
|
||||
<Row key={key}>
|
||||
{inputs.map(input => {
|
||||
return (
|
||||
<div className="col-12" key={input.name}>
|
||||
<Inputs
|
||||
type="text"
|
||||
onChange={onChange}
|
||||
{...input}
|
||||
label={formatMessage(input.label)}
|
||||
description={
|
||||
input.description
|
||||
? formatMessage(input.description)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</FormWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
<button type="submit" style={{ display: 'none' }}>
|
||||
hidden button to make to get the native form event
|
||||
</button>
|
||||
</ModalSection>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
EditForm.defaultProps = {
|
||||
fileToEdit: null,
|
||||
onChange: () => {},
|
||||
onClickDeleteFileToUpload: () => {},
|
||||
onSubmitEditNewFile: e => e.preventDefault(),
|
||||
setCropResult: () => {},
|
||||
};
|
||||
|
||||
EditForm.propTypes = {
|
||||
fileToEdit: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
onClickDeleteFileToUpload: PropTypes.func,
|
||||
onSubmitEditNewFile: PropTypes.func,
|
||||
setCropResult: PropTypes.func,
|
||||
};
|
||||
|
||||
export default EditForm;
|
@ -0,0 +1,37 @@
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
|
||||
const form = [
|
||||
{
|
||||
key: 1,
|
||||
inputs: [
|
||||
{
|
||||
label: { id: getTrad('form.input.label.file-name') },
|
||||
name: 'name',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
inputs: [
|
||||
{
|
||||
description: { id: getTrad('form.input.decription.file-alt') },
|
||||
label: { id: getTrad('form.input.label.file-alt') },
|
||||
name: 'alt',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
inputs: [
|
||||
{
|
||||
label: { id: getTrad('form.input.label.file-caption') },
|
||||
name: 'caption',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default form;
|
@ -0,0 +1,15 @@
|
||||
// Source: https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
|
||||
function formatBytes(bytes, decimals) {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals <= 0 ? 0 : decimals || 2;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export default formatBytes;
|
@ -0,0 +1,3 @@
|
||||
const isImageType = mimeType => mimeType.includes('image');
|
||||
|
||||
export default isImageType;
|
@ -0,0 +1,29 @@
|
||||
import formatBytes from '../formatBytes';
|
||||
|
||||
describe('UPLOAD | components | EditForm | utils', () => {
|
||||
describe('formatBytes', () => {
|
||||
it('should return 0 Bytes', () => {
|
||||
expect(formatBytes(0)).toEqual('0 Bytes');
|
||||
});
|
||||
|
||||
it('should return 1KB if 1024 Bytes is passed', () => {
|
||||
expect(formatBytes(1024)).toEqual('1 KB');
|
||||
});
|
||||
|
||||
it("should return 1KB if '1024' Bytes is passed", () => {
|
||||
expect(formatBytes('1024')).toEqual('1 KB');
|
||||
});
|
||||
|
||||
it('should return 1.21KB if 1034 Bytes is passed', () => {
|
||||
expect(formatBytes(1234)).toEqual('1.21 KB');
|
||||
});
|
||||
|
||||
it('should return 1.21KB if 1034 Bytes is passed with 3 decimals', () => {
|
||||
expect(formatBytes(1234, 3)).toEqual('1.205 KB');
|
||||
});
|
||||
|
||||
it('should return 1 MB if 1.1e+6 Bytes is passed', () => {
|
||||
expect(formatBytes(1100000, 0)).toEqual('1 MB');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,17 @@
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
justify-content: ${({ justifyContent }) => justifyContent};
|
||||
`;
|
||||
|
||||
Flex.defaultProps = {
|
||||
justifyContent: 'normal',
|
||||
};
|
||||
|
||||
Flex.propTypes = {
|
||||
justifyContent: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Flex;
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
*
|
||||
*
|
||||
* BackButton
|
||||
*
|
||||
*/
|
||||
import styled from 'styled-components';
|
||||
|
||||
const BackButton = styled.button`
|
||||
height: 6rem;
|
||||
width: 6.5rem;
|
||||
margin-right: 20px;
|
||||
margin-left: -30px;
|
||||
line-height: 6rem;
|
||||
text-align: center;
|
||||
color: #81848a;
|
||||
border-right: 1px solid #f3f4f4;
|
||||
&:before {
|
||||
content: '\f053';
|
||||
font-family: 'FontAwesome';
|
||||
font-size: ${({ theme }) => theme.main.fontSizes.lg};
|
||||
font-weight: ${({ theme }) => theme.main.fontWeights.bold};
|
||||
}
|
||||
&:hover {
|
||||
background-color: #f3f4f4;
|
||||
}
|
||||
`;
|
||||
|
||||
export default BackButton;
|
@ -8,17 +8,35 @@
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { HeaderModalTitle } from 'strapi-helper-plugin';
|
||||
import ModalSection from '../ModalSection';
|
||||
import Text from '../Text';
|
||||
import BackButton from './BackButton';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const ModalHeader = ({ headers }) => {
|
||||
const ModalHeader = ({ goBack, headers, withBackButton }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<ModalSection>
|
||||
<HeaderModalTitle>
|
||||
{headers.map(({ key, element }) => {
|
||||
return <Fragment key={key}>{element}</Fragment>;
|
||||
{withBackButton && <BackButton onClick={goBack} type="button" />}
|
||||
{headers.map(({ key, element }, index) => {
|
||||
const shouldDisplayChevron = index < headers.length - 1;
|
||||
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
{element}
|
||||
{shouldDisplayChevron && (
|
||||
<Text as="span" fontSize="xs" color="#919bae">
|
||||
<FontAwesomeIcon
|
||||
icon="chevron-right"
|
||||
style={{ margin: '0 10px' }}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</HeaderModalTitle>
|
||||
</ModalSection>
|
||||
@ -27,11 +45,15 @@ const ModalHeader = ({ headers }) => {
|
||||
};
|
||||
|
||||
ModalHeader.defaultProps = {
|
||||
goBack: () => {},
|
||||
headers: [],
|
||||
withBackButton: false,
|
||||
};
|
||||
|
||||
ModalHeader.propTypes = {
|
||||
goBack: PropTypes.func,
|
||||
headers: PropTypes.array,
|
||||
withBackButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ModalHeader;
|
||||
|
@ -7,6 +7,10 @@ const SortList = styled.ul`
|
||||
min-width: 230px;
|
||||
list-style-type: none;
|
||||
font-size: ${({ theme }) => theme.main.fontSizes.md};
|
||||
background-color: ${({ theme }) => theme.main.colors.white};
|
||||
border: 1px solid ${({ theme }) => theme.main.colors.darkGrey};
|
||||
box-shadow: 0 2px 4px ${({ theme }) => theme.main.colors.greyAlpha};
|
||||
${({ isOpen }) => isOpen && 'display: block;'}
|
||||
`;
|
||||
|
||||
SortList.propTypes = {
|
||||
|
@ -8,13 +8,12 @@ const SortListItem = styled.li`
|
||||
line-height: 27px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: ${({ theme }) =>
|
||||
theme.main.colors.filters.list.hover.background};
|
||||
background-color: ${({ theme }) => theme.main.colors.mediumGrey};
|
||||
}
|
||||
${({ isActive, theme }) =>
|
||||
isActive &&
|
||||
`
|
||||
background-color: ${theme.main.colors.filters.list.hover.background};
|
||||
background-color: ${theme.main.colors.mediumGrey};
|
||||
`}
|
||||
`;
|
||||
|
||||
|
@ -25,7 +25,7 @@ const SortPicker = ({ onChange, value }) => {
|
||||
};
|
||||
|
||||
const handleChange = value => {
|
||||
onChange({ target: { value } });
|
||||
onChange({ target: { name: '_sort', value } });
|
||||
|
||||
hangleToggle();
|
||||
};
|
||||
|
@ -3,15 +3,17 @@ import styled from 'styled-components';
|
||||
const Text = styled.p`
|
||||
margin: 0;
|
||||
line-height: 18px;
|
||||
color: ${({ theme, color }) => theme.main.colors[color]};
|
||||
color: ${({ theme, color }) => theme.main.colors[color] || color};
|
||||
font-size: ${({ theme, fontSize }) => theme.main.fontSizes[fontSize]};
|
||||
font-weight: ${({ theme, fontWeight }) => theme.main.fontWeights[fontWeight]};
|
||||
text-transform: ${({ textTransform }) => textTransform};
|
||||
`;
|
||||
|
||||
Text.defaultProps = {
|
||||
color: 'greyDark',
|
||||
fontSize: 'md',
|
||||
fontWeight: 'regular',
|
||||
textTransform: 'none',
|
||||
};
|
||||
|
||||
export default Text;
|
||||
|
@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
import ContainerFluid from '../ContainerFluid';
|
||||
|
||||
const Container = styled(ContainerFluid)`
|
||||
margin-bottom: 4px;
|
||||
padding-top: 14px;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
export default Container;
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CardControl from '../CardControl';
|
||||
import CardControlsWrapper from '../CardControlsWrapper';
|
||||
import CardImgWrapper from '../CardImgWrapper';
|
||||
import InfiniteLoadingIndicator from '../InfiniteLoadingIndicator';
|
||||
|
||||
@ -9,17 +11,27 @@ const RowItem = ({
|
||||
errorMessage,
|
||||
isUploading,
|
||||
onClick,
|
||||
onClickEdit,
|
||||
originalIndex,
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
onClick(originalIndex);
|
||||
};
|
||||
|
||||
const handleClickEdit = () => {
|
||||
onClickEdit(originalIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="col-3" key={originalIndex}>
|
||||
<div>
|
||||
<CardImgWrapper isSmall hasError={hasError}>
|
||||
{isUploading && <InfiniteLoadingIndicator onClick={handleClick} />}
|
||||
{!isUploading && (
|
||||
<CardControlsWrapper className="card-control-wrapper">
|
||||
<CardControl onClick={handleClickEdit} />
|
||||
</CardControlsWrapper>
|
||||
)}
|
||||
</CardImgWrapper>
|
||||
<p style={{ marginBottom: 14 }}>{errorMessage || file.name}</p>
|
||||
</div>
|
||||
@ -37,6 +49,7 @@ RowItem.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
isUploading: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onClickEdit: PropTypes.func.isRequired,
|
||||
originalIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
|
@ -4,17 +4,17 @@ import { FormattedMessage } from 'react-intl';
|
||||
import { Button } from '@buffetjs/core';
|
||||
import createMatrix from '../../utils/createMatrix';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import ContainerFluid from '../ContainerFluid';
|
||||
import ModalSection from '../ModalSection';
|
||||
import Text from '../Text';
|
||||
import Container from './Container';
|
||||
import ButtonWrapper from './ButtonWrapper';
|
||||
import TextWrapper from './TextWrapper';
|
||||
import RowItem from './RowItem';
|
||||
import ListWrapper from './ListWrapper';
|
||||
|
||||
const UploadList = ({
|
||||
filesToUpload,
|
||||
onClickCancelUpload,
|
||||
onClickEditNewFile,
|
||||
onGoToAddBrowseFiles,
|
||||
}) => {
|
||||
const matrix = createMatrix(filesToUpload);
|
||||
@ -56,23 +56,22 @@ const UploadList = ({
|
||||
</ModalSection>
|
||||
|
||||
<ModalSection>
|
||||
<ContainerFluid>
|
||||
<ListWrapper>
|
||||
{matrix.map(({ key, rowContent }) => {
|
||||
return (
|
||||
<div className="row" key={key}>
|
||||
{rowContent.map(data => (
|
||||
<RowItem
|
||||
{...data}
|
||||
onClick={onClickCancelUpload}
|
||||
key={data.originalIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ListWrapper>
|
||||
</ContainerFluid>
|
||||
<Container>
|
||||
{matrix.map(({ key, rowContent }) => {
|
||||
return (
|
||||
<div className="row" key={key}>
|
||||
{rowContent.map(data => (
|
||||
<RowItem
|
||||
{...data}
|
||||
onClick={onClickCancelUpload}
|
||||
onClickEdit={onClickEditNewFile}
|
||||
key={data.originalIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
</ModalSection>
|
||||
</>
|
||||
);
|
||||
@ -81,12 +80,14 @@ const UploadList = ({
|
||||
UploadList.defaultProps = {
|
||||
filesToUpload: [],
|
||||
onClickCancelUpload: () => {},
|
||||
onClickEditNewFile: () => {},
|
||||
onGoToAddBrowseFiles: () => {},
|
||||
};
|
||||
|
||||
UploadList.propTypes = {
|
||||
filesToUpload: PropTypes.array,
|
||||
onClickCancelUpload: PropTypes.func,
|
||||
onClickEditNewFile: PropTypes.func,
|
||||
onGoToAddBrowseFiles: PropTypes.func,
|
||||
};
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useReducer, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import React, { useReducer, useState, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Header } from '@buffetjs/custom';
|
||||
import {
|
||||
HeaderSearch,
|
||||
PageFooter,
|
||||
useGlobalContext,
|
||||
useQuery,
|
||||
generateSearchFromFilters,
|
||||
} from 'strapi-helper-plugin';
|
||||
import useQuery from '../../hooks/useQuery';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import Container from '../../components/Container';
|
||||
import ControlsWrapper from '../../components/ControlsWrapper';
|
||||
@ -27,23 +27,17 @@ const HomePage = () => {
|
||||
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { push } = useHistory();
|
||||
const { search } = useLocation();
|
||||
const query = useQuery();
|
||||
const { data, dataToDelete, _limit, _page, _sort, _q } = reducerState.toJS();
|
||||
const { data, dataToDelete } = reducerState.toJS();
|
||||
const pluginName = formatMessage({ id: getTrad('plugin.name') });
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = getSearchParams();
|
||||
|
||||
Object.keys(searchParams).map(key => {
|
||||
return dispatch({
|
||||
type: 'ON_QUERY_CHANGE',
|
||||
key,
|
||||
value: searchParams[key],
|
||||
});
|
||||
// TODO - Retrieve data
|
||||
dispatch({
|
||||
type: 'GET_DATA_SUCCEEDED',
|
||||
data: [],
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [search]);
|
||||
}, []);
|
||||
|
||||
const getSearchParams = () => {
|
||||
const params = {};
|
||||
@ -64,38 +58,23 @@ const HomePage = () => {
|
||||
const handleChangeListParams = ({ target: { name, value } }) => {
|
||||
const key = name.split('.').pop();
|
||||
|
||||
handleChangeQuery({ key, value });
|
||||
handleChangeParams({ target: { name: key, value } });
|
||||
};
|
||||
|
||||
const handleChangeParams = ({ key, value }) => {
|
||||
const updatedSearch = getUpdatedSearchParams({ [key]: value });
|
||||
const getQueryValue = key => {
|
||||
const queryParams = getSearchParams();
|
||||
|
||||
return queryParams[key];
|
||||
};
|
||||
|
||||
const handleChangeParams = ({ target: { name, value } }) => {
|
||||
const updatedSearch = getUpdatedSearchParams({ [name]: value });
|
||||
const newSearch = generateSearchFromFilters(updatedSearch);
|
||||
push({ search: newSearch });
|
||||
};
|
||||
|
||||
const handleChangeSearch = ({ target: { value } }) => {
|
||||
handleChangeQuery({ key: '_q', value });
|
||||
};
|
||||
|
||||
const handleChangeSort = ({ target: { value } }) => {
|
||||
handleChangeQuery({ key: '_sort', value });
|
||||
};
|
||||
|
||||
const handleChangeQuery = ({ key, value }) => {
|
||||
dispatch({
|
||||
type: 'ON_QUERY_CHANGE',
|
||||
key,
|
||||
value,
|
||||
});
|
||||
|
||||
handleChangeParams({
|
||||
key,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
handleChangeSearch({ target: { value: '' } });
|
||||
handleChangeParams({ target: { name: '_q', value: '' } });
|
||||
};
|
||||
|
||||
const handleClickToggleModal = () => {
|
||||
@ -133,8 +112,8 @@ const HomePage = () => {
|
||||
};
|
||||
|
||||
const params = {
|
||||
_limit: parseInt(_limit, 10),
|
||||
_page: parseInt(_page, 10),
|
||||
_limit: parseInt(getQueryValue('_limit'), 10) || 10,
|
||||
_page: parseInt(getQueryValue('_page'), 10) || 1,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -142,15 +121,19 @@ const HomePage = () => {
|
||||
<Header {...headerProps} />
|
||||
<HeaderSearch
|
||||
label={pluginName}
|
||||
onChange={handleChangeSearch}
|
||||
onChange={handleChangeParams}
|
||||
onClear={handleClearSearch}
|
||||
placeholder={formatMessage({ id: getTrad('search.placeholder') })}
|
||||
value={_q}
|
||||
name="_q"
|
||||
value={getQueryValue('_q') || ''}
|
||||
/>
|
||||
|
||||
<ControlsWrapper>
|
||||
<SelectAll />
|
||||
<SortPicker onChange={handleChangeSort} value={_sort} />
|
||||
<SortPicker
|
||||
onChange={handleChangeParams}
|
||||
value={getQueryValue('_sort') || null}
|
||||
/>
|
||||
<FiltersPicker />
|
||||
<FiltersList />
|
||||
</ControlsWrapper>
|
||||
|
@ -3,16 +3,12 @@ import { fromJS } from 'immutable';
|
||||
const initialState = fromJS({
|
||||
data: [],
|
||||
dataToDelete: [],
|
||||
_q: '',
|
||||
_page: 1,
|
||||
_limit: 10,
|
||||
_sort: '',
|
||||
});
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'ON_QUERY_CHANGE':
|
||||
return state.update(action.key, () => action.value);
|
||||
case 'GET_DATA_SUCCEEDED':
|
||||
return state.update('data', () => action.data);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -19,8 +19,10 @@ import getTrad from '../../utils/getTrad';
|
||||
const ModalStepper = ({ isOpen, onToggle }) => {
|
||||
const { formatMessage } = useGlobalContext();
|
||||
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
|
||||
const { currentStep, filesToUpload } = reducerState.toJS();
|
||||
const { Component, headerTradId, next, prev } = stepper[currentStep];
|
||||
const { currentStep, fileToEdit, filesToUpload } = reducerState.toJS();
|
||||
const { Component, headers, next, prev, withBackButton } = stepper[
|
||||
currentStep
|
||||
];
|
||||
const filesToUploadLength = filesToUpload.length;
|
||||
const toggleRef = useRef();
|
||||
toggleRef.current = onToggle;
|
||||
@ -53,12 +55,36 @@ const ModalStepper = ({ isOpen, onToggle }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickDeleteFileToUpload = fileIndex => {
|
||||
dispatch({
|
||||
type: 'REMOVE_FILE_TO_UPLOAD',
|
||||
fileIndex,
|
||||
});
|
||||
|
||||
if (currentStep === 'edit-new') {
|
||||
dispatch({
|
||||
type: 'RESET_FILE_TO_EDIT',
|
||||
});
|
||||
|
||||
goNext();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClosed = () => {
|
||||
dispatch({
|
||||
type: 'RESET_PROPS',
|
||||
});
|
||||
};
|
||||
|
||||
const handleGoToEditNewFile = fileIndex => {
|
||||
dispatch({
|
||||
type: 'SET_FILE_TO_EDIT',
|
||||
fileIndex,
|
||||
});
|
||||
|
||||
goTo('edit-new');
|
||||
};
|
||||
|
||||
const handleGoToAddBrowseFiles = () => {
|
||||
dispatch({
|
||||
type: 'CLEAN_FILES_ERROR',
|
||||
@ -67,6 +93,38 @@ const ModalStepper = ({ isOpen, onToggle }) => {
|
||||
goBack();
|
||||
};
|
||||
|
||||
const handleSetCropResult = blob => {
|
||||
dispatch({
|
||||
type: 'SET_CROP_RESULT',
|
||||
blob,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitEditNewFile = e => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch({
|
||||
type: 'ON_SUBMIT_EDIT_NEW_FILE',
|
||||
});
|
||||
|
||||
goNext();
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
if (filesToUploadLength > 0) {
|
||||
// eslint-disable-next-line no-alert
|
||||
const confirm = window.confirm(
|
||||
formatMessage({ id: getTrad('window.confirm.close-modal') })
|
||||
);
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onToggle();
|
||||
};
|
||||
|
||||
const handleUploadFiles = async () => {
|
||||
dispatch({
|
||||
type: 'SET_FILES_UPLOADING_STATE',
|
||||
@ -138,29 +196,36 @@ const ModalStepper = ({ isOpen, onToggle }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onToggle={onToggle} onClosed={handleClosed}>
|
||||
<Modal isOpen={isOpen} onToggle={handleToggle} onClosed={handleClosed}>
|
||||
{/* header title */}
|
||||
<ModalHeader
|
||||
headers={[
|
||||
{
|
||||
key: headerTradId,
|
||||
element: <FormattedMessage id={headerTradId} />,
|
||||
},
|
||||
]}
|
||||
goBack={goBack}
|
||||
headers={headers.map(headerTrad => ({
|
||||
key: headerTrad,
|
||||
element: <FormattedMessage id={headerTrad} />,
|
||||
}))}
|
||||
withBackButton={withBackButton}
|
||||
/>
|
||||
{/* body of the modal */}
|
||||
{Component && (
|
||||
<Component
|
||||
addFilesToUpload={addFilesToUpload}
|
||||
fileToEdit={fileToEdit}
|
||||
filesToUpload={filesToUpload}
|
||||
onClickCancelUpload={handleCancelFileToUpload}
|
||||
onClickDeleteFileToUpload={handleClickDeleteFileToUpload}
|
||||
onClickEditNewFile={handleGoToEditNewFile}
|
||||
onGoToAddBrowseFiles={handleGoToAddBrowseFiles}
|
||||
onSubmitEditNewFile={handleSubmitEditNewFile}
|
||||
onToggle={handleToggle}
|
||||
setCropResult={handleSetCropResult}
|
||||
withBackButton={withBackButton}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ModalFooter>
|
||||
<section>
|
||||
<Button type="button" color="cancel" onClick={onToggle}>
|
||||
<Button type="button" color="cancel" onClick={handleToggle}>
|
||||
{formatMessage({ id: 'app.components.Button.cancel' })}
|
||||
</Button>
|
||||
{currentStep === 'upload' && (
|
||||
@ -177,6 +242,15 @@ const ModalStepper = ({ isOpen, onToggle }) => {
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 'edit-new' && (
|
||||
<Button
|
||||
color="success"
|
||||
type="button"
|
||||
onClick={handleSubmitEditNewFile}
|
||||
>
|
||||
{formatMessage({ id: 'form.button.finish' })}
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
@ -4,6 +4,7 @@ import createNewFilesToUploadArray from './utils/createNewFilesToUploadArray';
|
||||
const initialState = fromJS({
|
||||
currentStep: 'browse',
|
||||
filesToUpload: [],
|
||||
fileToEdit: null,
|
||||
});
|
||||
|
||||
const reducer = (state, action) => {
|
||||
@ -24,14 +25,28 @@ const reducer = (state, action) => {
|
||||
);
|
||||
case 'GO_TO':
|
||||
return state.update('currentStep', () => action.to);
|
||||
case 'ON_SUBMIT_EDIT_NEW_FILE': {
|
||||
const originalIndex = state.getIn(['fileToEdit', 'originalIndex']);
|
||||
|
||||
return state
|
||||
.updateIn(['filesToUpload', originalIndex], () =>
|
||||
state.get('fileToEdit')
|
||||
)
|
||||
.update('fileToEdit', () => null);
|
||||
}
|
||||
case 'REMOVE_FILE_TO_UPLOAD':
|
||||
return state.update('filesToUpload', list => {
|
||||
return list.filter(
|
||||
data => data.get('originalIndex') !== action.fileIndex
|
||||
);
|
||||
});
|
||||
case 'RESET_FILE_TO_EDIT':
|
||||
return state.update('fileToEdit', () => null);
|
||||
case 'RESET_PROPS':
|
||||
return initialState;
|
||||
case 'SET_CROP_RESULT': {
|
||||
return state.updateIn(['fileToEdit', 'file'], () => fromJS(action.blob));
|
||||
}
|
||||
case 'SET_FILE_ERROR':
|
||||
return state.update('filesToUpload', list => {
|
||||
return list.map(data => {
|
||||
@ -45,6 +60,10 @@ const reducer = (state, action) => {
|
||||
return data;
|
||||
});
|
||||
});
|
||||
case 'SET_FILE_TO_EDIT':
|
||||
return state.update('fileToEdit', () =>
|
||||
state.getIn(['filesToUpload', action.fileIndex])
|
||||
);
|
||||
case 'SET_FILES_UPLOADING_STATE':
|
||||
return state.update('filesToUpload', list =>
|
||||
list.map(data =>
|
||||
|
@ -244,6 +244,71 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ON_SUBMIT_EDIT_NEW_FILE', () => {
|
||||
it('should update the filesToUploadList with the fileToEdit data', () => {
|
||||
const action = {
|
||||
type: 'ON_SUBMIT_EDIT_NEW_FILE',
|
||||
};
|
||||
const state = fromJS({
|
||||
currentStep: 'edit-new',
|
||||
filesToUpload: [
|
||||
{
|
||||
originalIndex: 0,
|
||||
file: {
|
||||
name: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
test: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 2,
|
||||
file: {
|
||||
name: 'test2',
|
||||
},
|
||||
},
|
||||
],
|
||||
fileToEdit: {
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
test: true,
|
||||
otherTest: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const expected = fromJS({
|
||||
currentStep: 'edit-new',
|
||||
filesToUpload: [
|
||||
{
|
||||
originalIndex: 0,
|
||||
file: {
|
||||
name: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
test: true,
|
||||
otherTest: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 2,
|
||||
file: {
|
||||
name: 'test2',
|
||||
},
|
||||
},
|
||||
],
|
||||
fileToEdit: null,
|
||||
});
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REMOVE_FILE_TO_UPLOAD', () => {
|
||||
it('should remove the file from the filesToUpload array', () => {
|
||||
const action = {
|
||||
@ -306,6 +371,22 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('RESET_FILE_TO_UPLOAD', () => {
|
||||
it('should set the fileToEdit key to null', () => {
|
||||
const action = {
|
||||
type: 'RESET_FILE_TO_EDIT',
|
||||
};
|
||||
const state = fromJS({
|
||||
fileToEdit: 'test',
|
||||
});
|
||||
const expected = fromJS({
|
||||
fileToEdit: null,
|
||||
});
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RESET_PROPS', () => {
|
||||
it('should return the initialState', () => {
|
||||
const action = { type: 'RESET_PROPS' };
|
||||
@ -313,6 +394,34 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
|
||||
const expected = fromJS({
|
||||
currentStep: 'browse',
|
||||
filesToUpload: [],
|
||||
fileToEdit: null,
|
||||
});
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_CROP_RESULT', () => {
|
||||
it('should update the fileToEditEntry with the passed data', () => {
|
||||
const action = {
|
||||
type: 'SET_CROP_RESULT',
|
||||
blob: {
|
||||
test: true,
|
||||
},
|
||||
};
|
||||
const state = fromJS({
|
||||
fileToEdit: {
|
||||
originalIndex: 1,
|
||||
file: null,
|
||||
},
|
||||
});
|
||||
const expected = fromJS({
|
||||
fileToEdit: {
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
test: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
@ -390,6 +499,68 @@ describe('UPLOAD | containers | ModalStepper | reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_FILE_TO_EDIT', () => {
|
||||
it('should set the fileToEdit key with the file at the passed index from the filesToUpload list', () => {
|
||||
const action = {
|
||||
type: 'SET_FILE_TO_EDIT',
|
||||
fileIndex: 1,
|
||||
};
|
||||
const state = fromJS({
|
||||
fileToEdit: null,
|
||||
filesToUpload: [
|
||||
{
|
||||
originalIndex: 0,
|
||||
file: {
|
||||
name: 'test0',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
name: 'test1',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 2,
|
||||
file: {
|
||||
name: 'test2',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const expected = fromJS({
|
||||
fileToEdit: {
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
name: 'test1',
|
||||
},
|
||||
},
|
||||
filesToUpload: [
|
||||
{
|
||||
originalIndex: 0,
|
||||
file: {
|
||||
name: 'test0',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 1,
|
||||
file: {
|
||||
name: 'test1',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalIndex: 2,
|
||||
file: {
|
||||
name: 'test2',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_FILES_UPLOADING_STATE', () => {
|
||||
it('should change all the isUploading keys of the filesToUpload to true', () => {
|
||||
const action = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import EditForm from '../../../components/EditForm';
|
||||
import UploadForm from '../../../components/UploadForm';
|
||||
import UploadList from '../../../components/UploadList';
|
||||
import getTrad from '../../../utils/getTrad';
|
||||
@ -5,21 +6,28 @@ import getTrad from '../../../utils/getTrad';
|
||||
const stepper = {
|
||||
browse: {
|
||||
Component: UploadForm,
|
||||
headerTradId: getTrad('modal.header.browse'),
|
||||
headers: [getTrad('modal.header.browse')],
|
||||
prev: null,
|
||||
next: 'upload',
|
||||
},
|
||||
upload: {
|
||||
Component: UploadList,
|
||||
headerTradId: getTrad('modal.header.select-files'),
|
||||
headers: [getTrad('modal.header.select-files')],
|
||||
next: null,
|
||||
prev: 'browse',
|
||||
},
|
||||
'edit-new': {
|
||||
Component: null,
|
||||
headerTradId: 'coming soon',
|
||||
next: null,
|
||||
Component: EditForm,
|
||||
// TODO: I'll leave it there for the moment
|
||||
// because I am not sure about the design since it seems inconsistent
|
||||
// headers: [
|
||||
// getTrad('modal.header.select-files'),
|
||||
// getTrad('modal.header.file-detail'),
|
||||
// ],
|
||||
headers: [getTrad('modal.header.file-detail')],
|
||||
next: 'upload',
|
||||
prev: 'upload',
|
||||
withBackButton: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const useQuery = () => {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
};
|
||||
|
||||
export default useQuery;
|
@ -1,8 +1,10 @@
|
||||
import pluginPkg from '../../package.json';
|
||||
import pluginLogo from './assets/images/logo.svg';
|
||||
import App from './containers/App';
|
||||
|
||||
import trads from './translations';
|
||||
import pluginId from './pluginId';
|
||||
import getTrad from './utils/getTrad';
|
||||
|
||||
export default strapi => {
|
||||
const pluginDescription =
|
||||
@ -24,6 +26,20 @@ export default strapi => {
|
||||
name: pluginPkg.strapi.name,
|
||||
pluginLogo,
|
||||
preventComponentRendering: false,
|
||||
settings: {
|
||||
global: [
|
||||
{
|
||||
title: {
|
||||
id: getTrad('settings.link.label'),
|
||||
defaultMessage: 'Media Library',
|
||||
},
|
||||
name: 'media-library',
|
||||
to: `${strapi.settingsBaseURL}/media-library`,
|
||||
// TODO
|
||||
Component: () => 'COMING SOON',
|
||||
},
|
||||
],
|
||||
},
|
||||
trads,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,8 @@
|
||||
{
|
||||
"form.input.label.file-alt": "Alternative text",
|
||||
"form.input.decription.file-alt": "This text will be displayed if the asset can’t be shown.",
|
||||
"form.input.label.file-caption": "Caption",
|
||||
"form.input.label.file-name": "File name",
|
||||
"header.actions.upload-assets": "Upload assets",
|
||||
"header.content.assets-empty": "No asset",
|
||||
"header.content.assets-multiple": "{number} assets",
|
||||
@ -10,6 +14,7 @@
|
||||
"list.assets-empty.subtitle": "Add a first one to the list.",
|
||||
"modal.header.browse": "Upload assets",
|
||||
"modal.header.select-files": "Selected files",
|
||||
"modal.header.file-detail": "Details",
|
||||
"modal.nav.computer": "from computer",
|
||||
"modal.nav.url": "from url",
|
||||
"modal.upload-list.sub-header-title.plural": "{number} assets selected",
|
||||
@ -28,5 +33,6 @@
|
||||
"sort.name_asc": "Alphabetical order (A to Z)",
|
||||
"sort.name_desc": "Reverse alphabetical order (Z to A)",
|
||||
"sort.updated_at_asc": "Most recent updates",
|
||||
"sort.updated_at_desc": "Oldest updates"
|
||||
}
|
||||
"sort.updated_at_desc": "Oldest updates",
|
||||
"window.confirm.close-modal": "Are you sure? You have some files that have not been uploaded yet."
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
"test": "echo \"no tests yet\""
|
||||
},
|
||||
"dependencies": {
|
||||
"cropperjs": "^1.5.6",
|
||||
"immutable": "^3.8.2",
|
||||
"invariant": "^2.2.1",
|
||||
"lodash": "^4.17.11",
|
||||
|
Loading…
x
Reference in New Issue
Block a user