feat: Add gallery for attachments
parent
0be598ca9e
commit
86e4864d1b
@ -1,88 +1,153 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from 'semantic-ui-react';
|
import { Gallery, Item as GalleryItem } from 'react-photoswipe-gallery';
|
||||||
|
import { Button, Grid } from 'semantic-ui-react';
|
||||||
import { useToggle } from '../../../lib/hooks';
|
import { useToggle } from '../../../lib/hooks';
|
||||||
|
|
||||||
import Item from './Item';
|
import Item from './Item';
|
||||||
|
|
||||||
import styles from './Attachments.module.scss';
|
import styles from './Attachments.module.scss';
|
||||||
|
|
||||||
const Attachments = React.memo(({ items, onUpdate, onDelete, onCoverUpdate }) => {
|
const INITIALLY_VISIBLE = 4;
|
||||||
const [t] = useTranslation();
|
|
||||||
const [isOpened, toggleOpened] = useToggle();
|
const Attachments = React.memo(
|
||||||
|
({ items, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => {
|
||||||
const handleToggleClick = useCallback(() => {
|
const [t] = useTranslation();
|
||||||
toggleOpened();
|
const [isAllVisible, toggleAllVisible] = useToggle();
|
||||||
}, [toggleOpened]);
|
|
||||||
|
const handleCoverSelect = useCallback(
|
||||||
const handleCoverSelect = useCallback(
|
(id) => {
|
||||||
(id) => {
|
onCoverUpdate(id);
|
||||||
onCoverUpdate(id);
|
},
|
||||||
},
|
[onCoverUpdate],
|
||||||
[onCoverUpdate],
|
);
|
||||||
);
|
|
||||||
|
const handleCoverDeselect = useCallback(() => {
|
||||||
const handleCoverDeselect = useCallback(() => {
|
onCoverUpdate(null);
|
||||||
onCoverUpdate(null);
|
}, [onCoverUpdate]);
|
||||||
}, [onCoverUpdate]);
|
|
||||||
|
const handleUpdate = useCallback(
|
||||||
const handleUpdate = useCallback(
|
(id, data) => {
|
||||||
(id, data) => {
|
onUpdate(id, data);
|
||||||
onUpdate(id, data);
|
},
|
||||||
},
|
[onUpdate],
|
||||||
[onUpdate],
|
);
|
||||||
);
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
const handleDelete = useCallback(
|
(id) => {
|
||||||
(id) => {
|
onDelete(id);
|
||||||
onDelete(id);
|
},
|
||||||
},
|
[onDelete],
|
||||||
[onDelete],
|
);
|
||||||
);
|
|
||||||
|
const handleBeforeGalleryOpen = useCallback(
|
||||||
const visibleItems = isOpened ? items : items.slice(0, 4);
|
(gallery) => {
|
||||||
|
onGalleryOpen();
|
||||||
return (
|
|
||||||
<>
|
gallery.on('destroy', () => {
|
||||||
{visibleItems.map((item) => (
|
onGalleryClose();
|
||||||
<Item
|
});
|
||||||
|
},
|
||||||
|
[onGalleryOpen, onGalleryClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleAllVisibleClick = useCallback(() => {
|
||||||
|
toggleAllVisible();
|
||||||
|
}, [toggleAllVisible]);
|
||||||
|
|
||||||
|
const galleryItemsNode = items.map((item, index) => {
|
||||||
|
const props = item.coverUrl
|
||||||
|
? {
|
||||||
|
width: item.imageWidth,
|
||||||
|
height: item.imageHeight,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
content: (
|
||||||
|
<Grid verticalAlign="middle" className={styles.contentWrapper}>
|
||||||
|
<Grid.Column textAlign="center" className={styles.content}>
|
||||||
|
{t('common.thereIsNoPreviewAvailableForThisAttachment')}
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const isVisible = isAllVisible || index < INITIALLY_VISIBLE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GalleryItem
|
||||||
|
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
key={item.id}
|
key={item.id}
|
||||||
name={item.name}
|
original={item.url}
|
||||||
url={item.url}
|
caption={item.name}
|
||||||
coverUrl={item.coverUrl}
|
>
|
||||||
createdAt={item.createdAt}
|
{({ ref, open }) =>
|
||||||
isCover={item.isCover}
|
isVisible ? (
|
||||||
isPersisted={item.isPersisted}
|
<Item
|
||||||
onCoverSelect={() => handleCoverSelect(item.id)}
|
ref={ref}
|
||||||
onCoverDeselect={handleCoverDeselect}
|
name={item.name}
|
||||||
onUpdate={(data) => handleUpdate(item.id, data)}
|
url={item.url}
|
||||||
onDelete={() => handleDelete(item.id)}
|
coverUrl={item.coverUrl}
|
||||||
/>
|
createdAt={item.createdAt}
|
||||||
))}
|
isCover={item.isCover}
|
||||||
{items.length > 4 && (
|
isPersisted={item.isPersisted}
|
||||||
<Button
|
onClick={item.coverUrl ? open : undefined}
|
||||||
fluid
|
onCoverSelect={() => handleCoverSelect(item.id)}
|
||||||
content={
|
onCoverDeselect={handleCoverDeselect}
|
||||||
isOpened
|
onUpdate={(data) => handleUpdate(item.id, data)}
|
||||||
? t('action.showFewerAttachments')
|
onDelete={() => handleDelete(item.id)}
|
||||||
: t('action.showAllAttachments', {
|
/>
|
||||||
hidden: items.length - visibleItems.length,
|
) : (
|
||||||
})
|
<span ref={ref} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className={styles.toggleButton}
|
</GalleryItem>
|
||||||
onClick={handleToggleClick}
|
);
|
||||||
/>
|
});
|
||||||
)}
|
|
||||||
</>
|
return (
|
||||||
);
|
<>
|
||||||
});
|
<Gallery
|
||||||
|
withCaption
|
||||||
|
withDownloadButton
|
||||||
|
options={{
|
||||||
|
showHideAnimationType: 'none',
|
||||||
|
closeTitle: '',
|
||||||
|
zoomTitle: '',
|
||||||
|
arrowPrevTitle: '',
|
||||||
|
arrowNextTitle: '',
|
||||||
|
errorMsg: '',
|
||||||
|
}}
|
||||||
|
onBeforeOpen={handleBeforeGalleryOpen}
|
||||||
|
>
|
||||||
|
{galleryItemsNode}
|
||||||
|
</Gallery>
|
||||||
|
{items.length > INITIALLY_VISIBLE && (
|
||||||
|
<Button
|
||||||
|
fluid
|
||||||
|
content={
|
||||||
|
isAllVisible
|
||||||
|
? t('action.showFewerAttachments')
|
||||||
|
: t('action.showAllAttachments', {
|
||||||
|
hidden: items.length - INITIALLY_VISIBLE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className={styles.toggleButton}
|
||||||
|
onClick={handleToggleAllVisibleClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Attachments.propTypes = {
|
Attachments.propTypes = {
|
||||||
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
onUpdate: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onCoverUpdate: PropTypes.func.isRequired,
|
onCoverUpdate: PropTypes.func.isRequired,
|
||||||
|
onGalleryOpen: PropTypes.func.isRequired,
|
||||||
|
onGalleryClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Attachments;
|
export default Attachments;
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
|
||||||
|
const getConfig = require('../../get-config');
|
||||||
|
|
||||||
|
module.exports.up = async (knex) => {
|
||||||
|
await knex.schema.table('attachment', (table) => {
|
||||||
|
/* Columns */
|
||||||
|
|
||||||
|
table.integer('image_width');
|
||||||
|
table.integer('image_height');
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = await getConfig();
|
||||||
|
const attachments = await knex('attachment');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (attachment of attachments) {
|
||||||
|
if (attachment.is_image) {
|
||||||
|
const image = sharp(
|
||||||
|
path.join(config.custom.attachmentsPath, attachment.dirname, attachment.filename),
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
try {
|
||||||
|
metadata = await image.metadata(); // eslint-disable-line no-await-in-loop
|
||||||
|
} catch (error) {
|
||||||
|
continue; // eslint-disable-line no-continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex('attachment')
|
||||||
|
.update({
|
||||||
|
image_width: metadata.width,
|
||||||
|
image_height: metadata.height,
|
||||||
|
})
|
||||||
|
.where('id', attachment.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.down = (knex) =>
|
||||||
|
knex.schema.table('attachment', (table) => {
|
||||||
|
table.dropColumns('image_width', 'image_height');
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
const dotenv = require('dotenv');
|
||||||
|
const sails = require('sails');
|
||||||
|
const rc = require('sails/accessible/rc');
|
||||||
|
|
||||||
|
process.chdir(__dirname);
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const config = rc('sails');
|
||||||
|
|
||||||
|
const getConfigPromise = new Promise((resolve) => {
|
||||||
|
sails.load(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
hooks: {
|
||||||
|
...config.hooks,
|
||||||
|
logger: false,
|
||||||
|
request: false,
|
||||||
|
views: false,
|
||||||
|
blueprints: false,
|
||||||
|
responses: false,
|
||||||
|
helpers: false,
|
||||||
|
pubsub: false,
|
||||||
|
policies: false,
|
||||||
|
services: false,
|
||||||
|
security: false,
|
||||||
|
i18n: false,
|
||||||
|
session: false,
|
||||||
|
http: false,
|
||||||
|
userhooks: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
resolve(sails.config);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = () => getConfigPromise;
|
||||||
Loading…
Reference in New Issue