diff --git a/src/actions/tag-actions.js b/src/actions/tag-actions.js index 280efd61f..f6470da63 100644 --- a/src/actions/tag-actions.js +++ b/src/actions/tag-actions.js @@ -9,10 +9,9 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ + * */ import T from "i18n-react/dist/i18n-react"; -import history from "../history"; import { getRequest, putRequest, @@ -26,7 +25,9 @@ import { authErrorHandler, escapeFilterValue } from "openstack-uicore-foundation/lib/utils/actions"; +import history from "../history"; import { getAccessTokenSafely } from "../utils/methods"; +import { DEFAULT_PER_PAGE } from "../utils/constants"; export const REQUEST_TAGS = "REQUEST_TAGS"; export const RECEIVE_TAGS = "RECEIVE_TAGS"; @@ -54,8 +55,14 @@ export const TAG_ADDED_TO_GROUP = "TAG_ADDED_TO_GROUP"; export const TAG_REMOVED_FROM_GROUP = "TAG_REMOVED_FROM_GROUP"; export const getTags = - (term = null, page = 1, perPage = 10, order = "id", orderDir = 1) => - async (dispatch, getState) => { + ( + term = null, + page = 1, + perPage = DEFAULT_PER_PAGE, + order = "id", + orderDir = 1 + ) => + async (dispatch) => { const accessToken = await getAccessTokenSafely(); const filter = []; @@ -69,7 +76,7 @@ export const getTags = } const params = { - page: page, + page, per_page: perPage, access_token: accessToken }; @@ -81,7 +88,7 @@ export const getTags = // order if (order != null && orderDir != null) { const orderDirSign = orderDir === 1 ? "+" : "-"; - params["order"] = `${orderDirSign}${order}`; + params.order = `${orderDirSign}${order}`; } return getRequest( @@ -89,13 +96,13 @@ export const getTags = createAction(RECEIVE_TAGS), `${window.API_BASE_URL}/api/v1/tags`, authErrorHandler, - { order, orderDir, page, term } + { order, orderDir, page, term, perPage } )(params)(dispatch).then(() => { dispatch(stopLoading()); }); }; -export const getTag = (tagId) => async (dispatch, getState) => { +export const getTag = (tagId) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); dispatch(startLoading()); @@ -114,7 +121,7 @@ export const getTag = (tagId) => async (dispatch, getState) => { }); }; -export const deleteTag = (tagId) => async (dispatch, getState) => { +export const deleteTag = (tagId) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); dispatch(startLoading()); @@ -138,7 +145,7 @@ export const resetTagForm = () => (dispatch) => { dispatch(createAction(RESET_TAG_FORM)({})); }; -export const saveTag = (entity) => async (dispatch, getState) => { +export const saveTag = (entity) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); dispatch(startLoading()); @@ -149,7 +156,7 @@ export const saveTag = (entity) => async (dispatch, getState) => { const normalizedEntity = normalizeTag(entity); if (entity.id) { - putRequest( + return putRequest( createAction(UPDATE_TAG), createAction(TAG_UPDATED), `${window.API_BASE_URL}/api/v1/tags/${entity.id}`, @@ -159,28 +166,27 @@ export const saveTag = (entity) => async (dispatch, getState) => { )(params)(dispatch).then(() => { dispatch(showSuccessMessage(T.translate("edit_tag.tag_saved"))); }); - } else { - const success_message = { - title: T.translate("general.done"), - html: T.translate("edit_tag.tag_created"), - type: "success" - }; - - postRequest( - createAction(UPDATE_TAG), - createAction(TAG_ADDED), - `${window.API_BASE_URL}/api/v1/tags`, - normalizedEntity, - authErrorHandler, - entity - )(params)(dispatch).then(() => { - dispatch( - showMessage(success_message, () => { - history.push(`/app/tags`); - }) - ); - }); } + const success_message = { + title: T.translate("general.done"), + html: T.translate("edit_tag.tag_created"), + type: "success" + }; + + return postRequest( + createAction(UPDATE_TAG), + createAction(TAG_ADDED), + `${window.API_BASE_URL}/api/v1/tags`, + normalizedEntity, + authErrorHandler, + entity + )(params)(dispatch).then(() => { + dispatch( + showMessage(success_message, () => { + history.push("/app/tags"); + }) + ); + }); }; export const getTagGroups = () => async (dispatch, getState) => { @@ -207,7 +213,7 @@ export const getTagGroups = () => async (dispatch, getState) => { }; export const updateTagGroupsOrder = - (tagGroups, tagGroupId, newOrder) => async (dispatch, getState) => { + (tagGroups, tagGroupId) => async (dispatch, getState) => { const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; @@ -219,7 +225,7 @@ export const updateTagGroupsOrder = const tagGroup = tagGroups.find((tg) => tg.id === tagGroupId); delete tagGroup.allowed_tags; - putRequest( + return putRequest( null, createAction(TAG_GROUP_ORDER_UPDATED)(tagGroups), `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/track-tag-groups/${tagGroupId}`, @@ -252,7 +258,7 @@ export const getTagGroup = (tagGroupId) => async (dispatch, getState) => { }); }; -export const resetTagGroupForm = () => (dispatch, getState) => { +export const resetTagGroupForm = () => (dispatch) => { dispatch(createAction(RESET_TAG_GROUP_FORM)({})); }; @@ -274,7 +280,7 @@ export const saveTagGroup = (entity) => async (dispatch, getState) => { normalizedEntity, authErrorHandler, entity - )(params)(dispatch).then((payload) => { + )(params)(dispatch).then(() => { dispatch( showSuccessMessage(T.translate("edit_tag_group.tag_group_saved")) ); @@ -388,10 +394,8 @@ export const copyAllTagsToCategory = }); }; -export const createTag = (tag, callback) => async (dispatch, getState) => { - const { currentSummitState } = getState(); +export const createTag = (tag, callback) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); - const { currentSummit } = currentSummitState; const params = { access_token: accessToken @@ -403,7 +407,7 @@ export const createTag = (tag, callback) => async (dispatch, getState) => { null, createAction(TAG_CREATED), `${window.API_BASE_URL}/api/v1/tags`, - { tag: tag }, + { tag }, authErrorHandler )(params)(dispatch).then((payload) => { dispatch(stopLoading()); @@ -422,9 +426,9 @@ export const removeTagFromGroup = (tagId) => (dispatch) => { const normalizeTag = (entity) => { const normalizedEntity = { ...entity }; - delete normalizedEntity["created"]; - delete normalizedEntity["updated"]; - delete normalizedEntity["last_edited"]; + delete normalizedEntity.created; + delete normalizedEntity.updated; + delete normalizedEntity.last_edited; return normalizedEntity; }; diff --git a/src/components/forms/tag-form.js b/src/components/forms/tag-form.js deleted file mode 100644 index b4e09c2e4..000000000 --- a/src/components/forms/tag-form.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright 2017 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - -import React from "react"; -import T from "i18n-react/dist/i18n-react"; -import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"; -import { isEmpty, scrollToError, shallowEqual } from "../../utils/methods"; - -class TagForm extends React.Component { - constructor(props) { - super(props); - - this.state = { - entity: { ...props.entity }, - errors: props.errors - }; - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - const state = {}; - scrollToError(this.props.errors); - - if (!shallowEqual(prevProps.entity, this.props.entity)) { - state.entity = { ...this.props.entity }; - state.errors = {}; - } - - if (!shallowEqual(prevProps.errors, this.props.errors)) { - state.errors = { ...this.props.errors }; - } - - if (!isEmpty(state)) { - this.setState({ ...this.state, ...state }); - } - } - - handleChange(ev) { - const entity = { ...this.state.entity }; - const errors = { ...this.state.errors }; - let { value, id } = ev.target; - - errors[id] = ""; - entity[id] = value; - this.setState({ entity: entity, errors: errors }); - } - - handleSubmit(ev) { - ev.preventDefault(); - this.props.onSubmit(this.state.entity); - } - - render() { - const { entity } = this.state; - - return ( -
- -
-
- - -
-
- -
-
- -
-
-
- ); - } -} - -export default TagForm; diff --git a/src/i18n/en.json b/src/i18n/en.json index bf2730055..f4ccdd888 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -806,6 +806,8 @@ }, "tag_list": { "tag_list": "Tag List", + "item": "item", + "items": "items", "tags": "Tags", "add_tag": "Add Tag", "delete_tag_warning": "Are you sure you want to delete tag", @@ -818,6 +820,7 @@ "edit_tag": { "tag": "Tag", "name": "Name", + "name_required": "Name is required.", "tag_saved": "Tag saved successfully.", "tag_created": "Tag created successfully." }, diff --git a/src/layouts/tag-layout.js b/src/layouts/tag-layout.js index e3d273645..e2ba5c608 100644 --- a/src/layouts/tag-layout.js +++ b/src/layouts/tag-layout.js @@ -9,7 +9,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ + * */ import React from "react"; import { Switch, Route, withRouter } from "react-router-dom"; @@ -17,38 +17,22 @@ import T from "i18n-react/dist/i18n-react"; import { Breadcrumb } from "react-breadcrumbs"; import Restrict from "../routes/restrict"; -import EditTagPage from "../pages/tags/edit-tag-page"; import TagListPage from "../pages/tags/tag-list-page"; import NoMatchPage from "../pages/no-match-page"; -class TagLayout extends React.Component { - render() { - const { match } = this.props; - return ( -
- - - - - - - - -
- ); - } +function TagLayout(props) { + const { match } = props; + return ( +
+ + + + + +
+ ); } export default Restrict(withRouter(TagLayout), "tags"); diff --git a/src/pages/tags/edit-tag-page.js b/src/pages/tags/edit-tag-page.js deleted file mode 100644 index b91ca19fb..000000000 --- a/src/pages/tags/edit-tag-page.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright 2017 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React from "react"; -import { connect } from "react-redux"; -import T from "i18n-react/dist/i18n-react"; -import { Breadcrumb } from "react-breadcrumbs"; -import TagForm from "../../components/forms/tag-form"; -import { getTag, resetTagForm, saveTag } from "../../actions/tag-actions"; -import "../../styles/edit-company-page.less"; -import AddNewButton from "../../components/buttons/add-new-button"; - -class EditTagPage extends React.Component { - constructor(props) { - const tagId = props.match.params.tag_id; - super(props); - - if (!tagId) { - props.resetTagForm(); - } else { - props.getTag(tagId); - } - } - - componentDidUpdate(prevProps, prevState, snapshot) { - const oldId = prevProps.match.params.tag_id; - const newId = this.props.match.params.tag_id; - - if (oldId !== newId) { - if (!newId) { - this.props.resetTagForm(); - } else { - this.props.getTag(newId); - } - } - } - - render() { - const { entity, errors, match, saveTag } = this.props; - - const title = entity.id - ? T.translate("general.edit") - : T.translate("general.add"); - const breadcrumb = entity.id ? entity.name : T.translate("general.new"); - - return ( -
- -

- {title} {T.translate("edit_tag.tag")} - -

-
- -
- ); - } -} - -const mapStateToProps = ({ currentTagState }) => ({ - ...currentTagState -}); - -export default connect(mapStateToProps, { - getTag, - resetTagForm, - saveTag -})(EditTagPage); diff --git a/src/pages/tags/tag-list-page.js b/src/pages/tags/tag-list-page.js index 532938b65..60d413088 100644 --- a/src/pages/tags/tag-list-page.js +++ b/src/pages/tags/tag-list-page.js @@ -9,147 +9,186 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ + * */ -import React from "react"; +import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -import Swal from "sweetalert2"; -import { Pagination } from "react-bootstrap"; -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import Table from "openstack-uicore-foundation/lib/components/table"; -import { getTags, deleteTag } from "../../actions/tag-actions"; - -class TagListPage extends React.Component { - constructor(props) { - super(props); - - this.handleEdit = this.handleEdit.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.handlePageChange = this.handlePageChange.bind(this); - this.handleSort = this.handleSort.bind(this); - this.handleSearch = this.handleSearch.bind(this); - this.handleNewTag = this.handleNewTag.bind(this); - - this.state = {}; - } - - componentDidMount() { - const { term } = this.props; - this.props.getTags(term); - } - - handleEdit(tag_id) { - const { history } = this.props; - history.push(`/app/tags/${tag_id}`); - } - - handleDelete(tagId) { - const { deleteTag, tags } = this.props; - let tag = tags.find((s) => s.id === tagId); - - Swal.fire({ - title: T.translate("general.are_you_sure"), - text: T.translate("tag_list.delete_tag_warning") + " " + tag.tag, - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }).then(function (result) { - if (result.value) { - deleteTag(tagId); - } +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Grid2 from "@mui/material/Grid2"; +import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; +import MuiSearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; +import AddIcon from "@mui/icons-material/Add"; +import TagsDialog from "./tags-popup"; +import { + getTags, + deleteTag, + saveTag, + resetTagForm +} from "../../actions/tag-actions"; +import { DEFAULT_PER_PAGE } from "../../utils/constants"; + +const TagListPage = ({ + tags = [], + currentPage = 1, + perPage = DEFAULT_PER_PAGE, + order = "id", + orderDir = 1, + totalTags = 0, + getTags, + deleteTag, + saveTag, + resetTagForm +}) => { + const [search, setSearch] = useState(""); + const [dialogOpen, setDialogOpen] = useState(false); + const [editData, setEditData] = useState(null); + + useEffect(() => { + setSearch(""); + getTags("", 1, DEFAULT_PER_PAGE, "id", 1); + }, []); + + const handleSearch = (searchTerm) => { + setSearch(searchTerm); + getTags(searchTerm, 1, perPage, order, orderDir); + }; + + const handleNewTag = () => { + setEditData({}); + setDialogOpen(true); + }; + + const handleEditTag = (row) => { + setEditData(row); + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + setEditData(null); + resetTagForm(); + }; + + const handleSaveTag = (entity) => { + saveTag(entity).then(() => { + handleCloseDialog(); + getTags(search, currentPage, perPage, order, orderDir); + }); + }; + + const handleDeleteTag = (id) => { + deleteTag(id).then(() => { + getTags(search, currentPage, perPage, order, orderDir); }); - } - - handlePageChange(page) { - const { term, order, orderDir, perPage } = this.props; - this.props.getTags(term, page, perPage, order, orderDir); - } - - handleSort(index, key, dir, func) { - const { term, page, perPage } = this.props; - this.props.getTags(term, page, perPage, key, dir); - } - - handleSearch(term) { - const { order, orderDir, page, perPage } = this.props; - this.props.getTags(term, page, perPage, order, orderDir); - } - - handleNewTag(ev) { - const { history } = this.props; - history.push(`/app/tags/new`); - } - - render() { - const { tags, lastPage, currentPage, term, order, orderDir, totalTags } = - this.props; - - const columns = [ - { columnKey: "id", value: "Id", sortable: true }, - { columnKey: "tag", value: T.translate("general.name"), sortable: true }, - { columnKey: "created", value: T.translate("tag_list.created") }, - { columnKey: "updated", value: T.translate("tag_list.updated") } - ]; - - const table_options = { - sortCol: order, - sortDir: orderDir, - actions: { - edit: { onClick: this.handleEdit }, - delete: { onClick: this.handleDelete } - } - }; - - return ( -
-

- {" "} - {T.translate("tag_list.tag_list")} ({totalTags}){" "} -

-
-
- +

{T.translate("tag_list.tag_list")}

+ + + + + {totalTags}{" "} + {totalTags === 1 + ? T.translate("tag_list.item") + : T.translate("tag_list.items")} + + + + + -
-
- -
-
- - {tags.length > 0 && ( -
- - - - )} - - ); - } -} + + + + + + getTags(search, page, perPage, order, orderDir)} + onPerPageChange={(newPerPage) => + getTags(search, 1, newPerPage, order, orderDir) + } + onSort={(col, dir) => getTags(search, 1, perPage, col, dir)} + options={{ sortCol: order, sortDir: orderDir }} + onEdit={(row) => handleEditTag(row)} + onDelete={(id) => handleDeleteTag(id)} + getName={(row) => row.tag} + deleteDialogBody={(name) => + `${T.translate("tag_list.delete_tag_warning")} "${name}"?` + } + confirmButtonColor="error" + /> + + + + ); +}; const mapStateToProps = ({ currentTagListState }) => ({ ...currentTagListState @@ -157,5 +196,7 @@ const mapStateToProps = ({ currentTagListState }) => ({ export default connect(mapStateToProps, { getTags, - deleteTag + deleteTag, + saveTag, + resetTagForm })(TagListPage); diff --git a/src/pages/tags/tags-popup.js b/src/pages/tags/tags-popup.js new file mode 100644 index 000000000..268a3d584 --- /dev/null +++ b/src/pages/tags/tags-popup.js @@ -0,0 +1,91 @@ +import React, { useEffect } from "react"; +import T from "i18n-react/dist/i18n-react"; +import { FormikProvider, useFormik } from "formik"; +import * as yup from "yup"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import TextField from "@mui/material/TextField"; +import CloseIcon from "@mui/icons-material/Close"; + +const TagsDialog = ({ open, onClose, onSave, initialData }) => { + const formik = useFormik({ + initialValues: { + tag: initialData?.tag || "" + }, + validationSchema: yup.object({ + tag: yup.string().trim().required(T.translate("edit_tag.name_required")) + }), + enableReinitialize: true, + onSubmit: (values) => { + onSave({ ...initialData, tag: values.tag.trim() }); + } + }); + + useEffect(() => { + if (open) { + formik.resetForm({ + values: { + tag: initialData?.tag || "" + } + }); + } + }, [open, initialData?.id, initialData?.tag]); + + const handleClose = () => { + formik.resetForm(); + onClose(); + }; + + return ( + + + {initialData?.id + ? `${T.translate("general.edit")} ${T.translate("edit_tag.tag")}` + : `${T.translate("general.add")} ${T.translate("edit_tag.tag")}`} + + + + + + + + + + + + + + + + + + ); +}; + +export default TagsDialog; diff --git a/src/reducers/tags/tag-list-reducer.js b/src/reducers/tags/tag-list-reducer.js index 2191b3b4d..51d48e7bd 100644 --- a/src/reducers/tags/tag-list-reducer.js +++ b/src/reducers/tags/tag-list-reducer.js @@ -9,20 +9,19 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ + * */ +import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; + +import { epochToMoment } from "openstack-uicore-foundation/lib/utils/methods"; import { REQUEST_TAGS, RECEIVE_TAGS, TAG_DELETED } from "../../actions/tag-actions"; -import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; - -import { epochToMoment } from "openstack-uicore-foundation/lib/utils/methods"; - const DEFAULT_STATE = { - tags: {}, + tags: [], term: "", order: "id", orderDir: 1, @@ -39,12 +38,12 @@ const tagListReducer = (state = DEFAULT_STATE, action) => { return DEFAULT_STATE; } case REQUEST_TAGS: { - let { order, orderDir, term, page } = payload; - return { ...state, order, orderDir, term, currentPage: page }; + const { order, orderDir, term, page, perPage } = payload; + return { ...state, order, orderDir, term, currentPage: page, perPage }; } case RECEIVE_TAGS: { - let { current_page, total, last_page } = payload.response; - let tags = payload.response.data.map((t) => ({ + const { current_page, total, last_page } = payload.response; + const tags = payload.response.data.map((t) => ({ ...t, created: epochToMoment(t.created).format("MMMM Do YYYY, h:mm:ss a"), updated: epochToMoment(t.last_edited).format("MMMM Do YYYY, h:mm:ss a") @@ -52,14 +51,14 @@ const tagListReducer = (state = DEFAULT_STATE, action) => { return { ...state, - tags: tags, + tags, currentPage: current_page, totalTags: total, lastPage: last_page }; } case TAG_DELETED: { - let { tagId } = payload; + const { tagId } = payload; return { ...state, tags: state.tags.filter((s) => s.id !== tagId) }; } default: