Skip to content

Commit

Permalink
EZP-30525: Convert Browse Tab as a standalone ReactJS module
Browse files Browse the repository at this point in the history
  • Loading branch information
Piotr Nalepa committed May 13, 2019
1 parent 2baf3d0 commit 15bd637
Show file tree
Hide file tree
Showing 22 changed files with 1,166 additions and 2 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Expand Up @@ -34,6 +34,7 @@
"no-extra-boolean-cast": "off",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/require-default-props": "warn",
"jsx-quotes": ["error", "prefer-double"],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
Expand Down
5 changes: 4 additions & 1 deletion Resources/encore/ez.config.js
Expand Up @@ -11,5 +11,8 @@ module.exports = (Encore) => {
.addEntry('ezplatform-admin-ui-modules-content-tree-js', [
path.resolve(__dirname, '../../src/modules/content-tree/content.tree.module.js'),
])
.addEntry('ezplatform-admin-ui-modules-udw-v2-js', [path.resolve(__dirname, '../../src/modules/udw/udw.module.js')]);
.addEntry('ezplatform-admin-ui-modules-udw-v2-js', [path.resolve(__dirname, '../../src/modules/udw/udw.module.js')])
.addEntry('ezplatform-admin-ui-modules-udw-v2-tab-browse-js', [
path.resolve(__dirname, '../../src/modules/udw/tabs/browse/browse.tab.module.js'),
]);
};
2 changes: 2 additions & 0 deletions Resources/public/scss/modules/udw/_main.scss
@@ -1,3 +1,5 @@
@import './tabs/main';

.ez-udw-module {
.c-popup__body {
padding: calculateRem(16px);
Expand Down
1 change: 1 addition & 0 deletions Resources/public/scss/modules/udw/tabs/_main.scss
@@ -0,0 +1 @@
@import './browse/main';
31 changes: 31 additions & 0 deletions Resources/public/scss/modules/udw/tabs/browse/_main.scss
@@ -0,0 +1,31 @@
$browse-tab: '.ez-browse-tab';
#{$browse-tab} {
display: grid;
grid-template-areas:
'finder finder'
'items actions';
grid-template-columns: 1fr calculateRem(230px);
grid-gap: calculateRem(16px);

&--with-preview {
grid-template-areas:
'finder preview'
'items actions';
}

&__finder {
grid-area: finder;
}

&__preview {
grid-area: preview;
}

&__selected-items {
grid-area: items;
}

&__actions {
grid-area: actions;
}
}
Expand Up @@ -7,6 +7,7 @@
display: inline-block;
border-radius: calculateRem(4px);
background: $ez-ground-base-dark;
border: none;

&--any-item-selected {
cursor: pointer;
Expand All @@ -19,6 +20,7 @@
}

&__content-names {
display: block;
max-width: calculateRem(320px);
white-space: nowrap;
overflow: hidden;
Expand Down
@@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { checkIsBookmarked, addBookmark, removeBookmark } from '../../services/bookmark.service';
import Icon from '../../../common/icon/icon';
import LoadingSpinnerComponent from '../../../common/loading-spinner/loading.spinner.component';
import { showErrorNotification } from '../../../common/services/notification.service';
import { restInfo } from '../../../common/rest-info/rest.info';

const BookmarkIconComponent = ({ locationId }) => {
const [isBookmarked, setIsBookmarked] = useState(false);
const [isCheckingBookmarkedStatus, setIsCheckingBookmarkedStatus] = useState(false);
/**
* Required to change a bookmark state while in a location view of a bookmarked item
*
* @function dispatchBookmarkChangeEvent
*/
const dispatchBookmarkChangeEvent = () => {
const event = new CustomEvent('ez-bookmark-change', { detail: { bookmarked: isBookmarked, locationId } });

document.body.dispatchEvent(event);
};
const toggleBookmark = () => {
setIsCheckingBookmarkedStatus(true);

const bookmarkAction = isBookmarked ? removeBookmark : addBookmark;
const bookmarkToggled = new Promise((resolve) => bookmarkAction(restInfo, locationId, resolve));

bookmarkToggled
.then(() => {
setIsBookmarked(!isBookmarked);
setIsCheckingBookmarkedStatus(false);
})
.catch(showErrorNotification);
};
const bookmarkIconId = isBookmarked ? 'bookmark-active' : 'bookmark';
const action = isBookmarked ? 'remove' : 'add';
const iconExtraClasses = 'ez-icon--medium ez-icon--secondary';
const btnAttrs = {
type: 'button',
className: `c-bookmark-icon c-bookmark-icon--${action}`,
onClick: toggleBookmark,
disabled: isCheckingBookmarkedStatus,
};

useEffect(dispatchBookmarkChangeEvent, [isBookmarked]);
useEffect(() => {
setIsCheckingBookmarkedStatus(true);
checkIsBookmarked(restInfo, locationId, (isBookmarked) => {
setIsBookmarked(isBookmarked);
setIsCheckingBookmarkedStatus(false);
});
}, [locationId]);

const icon = isCheckingBookmarkedStatus ? (
<LoadingSpinnerComponent extraClasses={iconExtraClasses} />
) : (
<Icon name={bookmarkIconId} extraClasses={iconExtraClasses} />
);

return <button {...btnAttrs}>{icon}</button>;
};

BookmarkIconComponent.propTypes = {
locationId: PropTypes.string.isRequired,
location: PropTypes.object.isRequired,
};

export default BookmarkIconComponent;
@@ -0,0 +1,46 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import LoadingSpinnerComponent from '../../../common/loading-spinner/loading.spinner.component';

const getImageUri = (version) => {
if (!version) {
return '';
}

const imageField = version.Fields.field.find((field) => field.fieldTypeIdentifier === 'ezimage');

return imageField && imageField.fieldValue ? imageField.fieldValue.uri : '';
};

const ContentImagePreviewComponent = ({ version }) => {
if (!version) {
return <LoadingSpinnerComponent />;
}

const imageUri = getImageUri(version);
const imagePreviewNotAvailableLabel = Translator.trans(
/*@Desc("Content preview is not available")*/ 'content_meta_preview.image_preview_not_available.info',
{},
'universal_discovery_widget'
);

if (!imageUri.length) {
return <Fragment>{imagePreviewNotAvailableLabel}</Fragment>;
}

return <img className="c-content-image-preview" src={imageUri} alt="" />;
};

ContentImagePreviewComponent.propTypes = {
version: PropTypes.shape({
Fields: PropTypes.shape({
field: PropTypes.array.isRequired,
}).isRequired,
}),
};

ContentImagePreviewComponent.defaultProps = {
version: null,
};

export default ContentImagePreviewComponent;
@@ -0,0 +1,107 @@
import React from 'react';
import PropTypes from 'prop-types';
import ContentImagePreviewComponent from './content.image.preview.component';
import BookmarkIconComponent from './bookmark.icon.component';
import ContentTypeIconComponent from '../content-type-icon/content.type.icon.component';

const TEXT_LAST_MODIFIED = Translator.trans(
/*@Desc("Last modified")*/ 'content_meta_preview.last_modified.label',
{},
'universal_discovery_widget'
);
const TEXT_CREATION_DATE = Translator.trans(
/*@Desc("Creation date")*/ 'content_meta_preview.creation_date.label',
{},
'universal_discovery_widget'
);
const TEXT_TRANSLATIONS = Translator.trans(
/*@Desc("Translations")*/ 'content_meta_preview.translations.label',
{},
'universal_discovery_widget'
);
const TEXT_CONTAINER_TITLE = Translator.trans(
/*@Desc("Content Meta Preview")*/ 'content_meta_preview.title',
{},
'universal_discovery_widget'
);
const getTranslations = (location) => {
if (!location || !location.CurrentVersion) {
return [];
}

const languages = window.eZ.adminUiConfig.languages;
const version = location.CurrentVersion.Version;
const versionLanguages = version.VersionInfo.VersionTranslationInfo.Language;

return versionLanguages.map((language) => languages.mappings[language.languageCode].name);
};

const ContentMetaPreviewComponent = ({ location, maxHeight, isVisible }) => {
if (!isVisible) {
return null;
}

const renderTranslation = (translation, index) => {
return (
<span key={index} className="c-meta-preview__translation">
{translation}
</span>
);
};
const content = location.ContentInfo.Content;
let contentTypeIdentifier = null;
let contentTypeName = '';

if (content.ContentTypeInfo) {
contentTypeIdentifier = content.ContentTypeInfo.identifier;
contentTypeName = window.eZ.adminUiConfig.contentTypeNames[contentTypeIdentifier];
}

const { formatShortDateTime } = window.eZ.helpers.timezone;
const translations = getTranslations(location);
const version = location.CurrentVersion ? location.CurrentVersion.Version : null;

return (
<div className="c-meta-preview__wrapper" style={{ maxHeight: `${maxHeight}px` }}>
<h3 className="c-meta-preview__title">{TEXT_CONTAINER_TITLE}</h3>
<div className="c-meta-preview">
<div className="c-meta-preview__top-wrapper">
<div className="c-meta-preview__content-type">
<ContentTypeIconComponent identifier={contentTypeIdentifier} /> {contentTypeName}
</div>
<BookmarkIconComponent locationId={location.id} />
</div>
<div className="c-meta-preview__meta-wrapper">
<div className="c-meta-preview__image-wrapper">
<ContentImagePreviewComponent version={version} />
</div>
<div className="c-meta-preview__name">{content.Name}</div>
<div className="c-meta-preview__content-info">
<h3 className="c-meta-preview__subtitle">{TEXT_LAST_MODIFIED}:</h3>
{formatShortDateTime(new Date(content.lastModificationDate))}
</div>
<div className="c-meta-preview__content-info">
<h3 className="c-meta-preview__subtitle">{TEXT_CREATION_DATE}:</h3>
{formatShortDateTime(new Date(content.publishedDate))}
</div>
<div className="c-meta-preview__content-info">
<h3 className="c-meta-preview__subtitle">{TEXT_TRANSLATIONS}:</h3>
{translations.map(renderTranslation)}
</div>
</div>
</div>
</div>
);
};

ContentMetaPreviewComponent.propTypes = {
location: PropTypes.object.isRequired,
maxHeight: PropTypes.number.isRequired,
isVisible: PropTypes.bool,
};

ContentMetaPreviewComponent.defaultProps = {
isVisible: false,
};

export default ContentMetaPreviewComponent;
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../../../common/icon/icon';

const ContentTypeIconComponent = ({ identifier }) => {
if (!identifier) {
return null;
}

const contentTypeIconUrl = eZ.helpers.contentType.getContentTypeIconUrl(identifier);

return <Icon customPath={contentTypeIconUrl} extraClasses="c-content-type-icon ez-icon--small" />;
};

ContentTypeIconComponent.propTypes = {
identifier: PropTypes.string,
};

ContentTypeIconComponent.defaultProps = {
identifier: null,
};

export default ContentTypeIconComponent;
@@ -0,0 +1,74 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import Icon from '../../../common/icon/icon';
import { createCssClassNames } from '../../../common/css-class-names/css.class.names';

const SelectContentButtonComponent = ({ checkCanSelectContent, location, onSelect, onDeselect, isSelected }) => {
const [isSelectContentEnabled, setIsSelectContentEnabled] = useState(true);
const handleSelect = useCallback(
(event) => {
event.stopPropagation();

onSelect(location);
},
[location, onSelect]
);
const handleDeselect = useCallback(
(event) => {
event.stopPropagation();

onDeselect(location.id);
},
[location, onDeselect]
);
const toggleEnabledState = useCallback(
(selectContentEnabled) => {
if (isSelectContentEnabled === selectContentEnabled) {
return;
}

setIsSelectContentEnabled(selectContentEnabled);
},
[isSelectContentEnabled]
);

useEffect(() => {
checkCanSelectContent(location, toggleEnabledState);
}, [checkCanSelectContent, location, toggleEnabledState]);

const iconId = isSelected ? 'checkmark' : 'create';
const attrs = {
type: 'button',
className: createCssClassNames({
'c-select-content-button': true,
'c-select-content-button--selected': isSelected,
}),
onClick: isSelected ? handleDeselect : handleSelect,
};

if (!isSelected && !isSelectContentEnabled) {
return null;
}

return (
<button {...attrs}>
<Icon name={iconId} extraClasses="ez-icon--small" />
</button>
);
};

SelectContentButtonComponent.propTypes = {
checkCanSelectContent: PropTypes.func.isRequired,
location: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
onSelect: PropTypes.func.isRequired,
onDeselect: PropTypes.func.isRequired,
isSelected: PropTypes.bool,
};

SelectContentButtonComponent.defaultProps = {
isSelected: false,
};

export default SelectContentButtonComponent;

0 comments on commit 15bd637

Please sign in to comment.