34. Media Library - Manage Images

In this post we will create the ability to manage an image - update the metadata or delete the image.

Code 

https://github.com/fullsapps/media-library/tree/34.ManageImage

Background 

There are no new concepts introduced in this post we are continuing to follow the patterns we have seen throughout the application.

Walk Through

Let's start with something a little bit new. I want to display a bunch of the metadata we have stored for an image. Some of our important metadata is stored as Timestamps in Cloud Firestore. I want to create a really simple utility function that will convert the timestamp to a date that is much more human readable. I'm going to store this utility under our shared folder. If we had other utility functions we could store them similarly. Also, I am keeping the filename generic as we might end up with different datetime conversion functions.

shared/utils/datetime.js
export const convertTimestampToDate = (timestamp) => {
  return new Date(timestamp.seconds * 1000).toLocaleDateString();
};

We are going to need a bunch of things from the store to support managing an image. So, let's create a container version of our component to get what we need.

AdminPropertyImageContainer.jsx
import { connect } from 'react-redux';

import AdminPropertyImage from '../../../../components/admin/AdminProperty/AdminPropertyImage/AdminPropertyImage';
import {
  imageFetch,
  imageUpdate,
  imageDelete
} from '../../../../shared/redux/actions/images';
import { fetchSettings } from '../../../../shared/redux/actions/settings';

const mapStateToProps = (state, ownProps) => ({
  image: state.images.images.filter(
    (image) => image.id === ownProps.match.params.imageId
  )[0],
  loadingImages: state.images.loading,
  errorImages: state.images.error,
  settings: state.settings.settings,
  loadingSettings: state.settings.loading,
  errorSettings: state.settings.error
});

const mapDispatchToProps = (dispatch) => ({
  boundImageFetch: (imageId) => dispatch(imageFetch(imageId)),
  boundImageUpdate: (image) => dispatch(imageUpdate(image)),
  boundSettingsFetch: (type) => dispatch(fetchSettings(type)),
  boundImageDelete: (image) => dispatch(imageDelete(image))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(AdminPropertyImage);

Before we update the main component I want to create a helper component that will display read-only metadata that we want to show a user but not let them edit. This is an extremely straightforward component. Note that we are calling the utility function we created above to convert a timestamp to a more human readable date.

ReadOnlyMetadatat.jsx
import React from 'react';
import PropTypes from 'prop-types';
import { Header, List } from 'semantic-ui-react';

import { convertTimestampToDate } from '../../../../../shared/utils/datetime';

export default function ReadOnlyMetadata({ image }) {
  return (
    <>
      <Header content="Read-only metadata" size="medium" />
      <List>
        <List.Item>
          <List.Header>ID</List.Header>
          {image.id}
        </List.Item>
        <List.Item>
          <List.Header>File name</List.Header>
          {image.name}
        </List.Item>
        <List.Item>
          <List.Header>URL</List.Header>
          {image.url}
        </List.Item>
        <List.Item>
          <List.Header>Size (bytes)</List.Header>
          {image.size}
        </List.Item>
        <List.Item>
          <List.Header>Type</List.Header>
          {image.type}
        </List.Item>
        <List.Item>
          <List.Header>Uploaded</List.Header>
          {convertTimestampToDate(image.uploaded)}
        </List.Item>
      </List>
    </>
  );
}

ReadOnlyMetadata.propTypes = {
  image: PropTypes.object.isRequired
};

Now we can go ahead and pull together our AdminPropertyImage component.

Note that in addition to the read-only metadata component we are using the UploadImageForm to allow us to update existing data. I mentioned that we were going to do this in the last post. This is a pattern I like to follow when I can - leverage the same form for create and update (minimize duplication of code). I had to make a couple very minor adjustments to the prop-types we set previously to make this work.

AdminPropertyImage.jsx
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
  Button,
  Header,
  Dimmer,
  Loader,
  Container,
  Image,
  Grid,
  Segment,
  Confirm
} from 'semantic-ui-react';
import { Link } from 'react-router-dom';

import UploadImageForm from '../AdminPropertyImages/UploadImage/UploadImageForm';
import ReadOnlyMetadata from './ReadOnlyMetadata/ReadOnlyMetadata';
import * as routes from '../../../../shared/constants/routes';

export default function AdminPropertyImage({
  history,
  match,
  image,
  loadingImages,
  errorImages,
  boundImageFetch,
  boundImageUpdate,
  boundImageDelete,
  settings,
  loadingSettings,
  errorSettings,
  boundSettingsFetch
}) {
  const [deleteImageConfirmOpen, setDeleteImageConfirmOpen] = useState(false);

  useEffect(() => {
    if (!image) {
      boundImageFetch(match.params.imageId);
    }

    if (Object.keys(settings).length === 0) {
      boundSettingsFetch('imageMetadata');
    }
  }, []);

  const handleConfirm = () => {
    boundImageDelete(image);
    setDeleteImageConfirmOpen(!deleteImageConfirmOpen);
    history.goBack();
  };

  if (errorImages) {
    return <>Error! {errorImages}</>;
  }
  if (errorSettings) {
    return <>Error! {errorSettings}</>;
  }

  if (loadingImages || loadingSettings) {
    return (
      <>
        <Dimmer active>
          <Loader />
        </Dimmer>
      </>
    );
  }

  return (
    <>
      <Container>
        <Button
          content="All Images"
          icon="left arrow"
          labelPosition="left"
          as={Link}
          to={`${routes.ADMIN}${routes.ADMINPROPERTIES}/${
            match.params.propertyId
          }${routes.ADMINPROPERTYIMAGES}`}
        />

        {image && (
          <>
            {/* <Header as="h3" textAlign="center">
                {image.name}
              </Header> */}
            <Grid stackable padded columns={2}>
              <Grid.Row>
                <Grid.Column>
                  <Segment>
                    <Image src={image.url} />
                  </Segment>
                  <Segment>
                    <ReadOnlyMetadata image={image} />
                  </Segment>
                </Grid.Column>
                <Grid.Column>
                  <Segment>
                    <Header content="Configurable metadata" size="medium" />
                    <UploadImageForm
                      isUpload={false}
                      image={image}
                      imageUpdate={boundImageUpdate}
                    />
                  </Segment>
                  <Button
                    color="red"
                    id="deleteButton"
                    basic
                    compact
                    size="tiny"
                    onClick={() =>
                      setDeleteImageConfirmOpen(!deleteImageConfirmOpen)
                    }
                  >
                    Delete image?
                  </Button>
                </Grid.Column>
              </Grid.Row>
            </Grid>
          </>
        )}
      </Container>
      <Confirm
        open={deleteImageConfirmOpen}
        content={`Are you sure you want to delete this image?`}
        onCancel={() => setDeleteImageConfirmOpen(!deleteImageConfirmOpen)}
        onConfirm={handleConfirm}
        size="mini"
      />
    </>
  );
}

AdminPropertyImage.propTypes = {
  history: PropTypes.object,
  match: PropTypes.object.isRequired,
  image: PropTypes.object,
  loadingImages: PropTypes.bool,
  errorImages: PropTypes.string,
  boundImageFetch: PropTypes.func.isRequired,
  boundImageUpdate: PropTypes.func.isRequired,
  boundImageDelete: PropTypes.func.isRequired,
  settings: PropTypes.object.isRequired,
  loadingSettings: PropTypes.bool,
  errorSettings: PropTypes.string,
  boundSettingsFetch: PropTypes.func.isRequired
};

As always the last thing I need to do is swap out the non-container version of the component for the container version when the component gets rendered to have the state automatically attached. Note that we continue to follow a pattern that will allow links from our application to be shared and the application will work as expected.

AdminProperty.jsx (updates)
...
//import AdminPropertyImage from './AdminPropertyImage/AdminPropertyImage';
import AdminPropertyImageContainer from '../../../containers/admin/AdminProperty/AdminPropertyImage/AdminPropertyImageContainer';
...
        />
        <Route
          path={routes.ADMIN + routes.ADMINPROPERTY + routes.ADMINPROPERTYIMAGE}
          component={AdminPropertyImageContainer}
        />
...


In this branch I also did a tiny bit of cleanup to move a folder back to the correct location.

Next

It feels like we have been working on the admin side of this application for quite a while. Let's switch gears and attempt to add some value to the images we are loading.

Comments

Popular posts from this blog

Calling a REST API from AWS Lambda (The Easy Way)

Calling AWS AppSync, or any GraphQL API, from AWS Lambda, part 1

32. Media Library - Uploading Images, Part 1