Compare commits

...

5 Commits

Author SHA1 Message Date
Tudor Stanciu 89cf1fcd76 SecondaryActionsGroup 2022-12-12 01:57:23 +02:00
Tudor Stanciu 909f4df42a resources actions 2022-12-12 01:24:22 +02:00
Tudor Stanciu 000faf1b7d translations 2022-12-11 19:55:05 +02:00
Tudor Stanciu 94839a746b resources list 2022-12-09 18:38:01 +02:00
Tudor Stanciu 0814d09533 resources page fixes 2022-12-08 02:31:03 +02:00
10 changed files with 483 additions and 80 deletions

View File

@ -1,2 +1,3 @@
- schimbare limba din setari - schimbare limba din setari
- salvare configurari - pozitie toast - salvare configurari - pozitie toast

View File

@ -12,6 +12,37 @@
"English": "English", "English": "English",
"Romanian": "Romanian" "Romanian": "Romanian"
}, },
"Generic": {
"Table": {
"Body": {
"NoMatch": "Sorry, no matching records found"
},
"Filter": {
"All": "All",
"Reset": "Reset",
"Title": "FILTERS"
},
"Pagination": {
"DisplayRows": "of",
"Next": "Next page",
"Previous": "Previous page",
"RowsPerPage": "Rows per page"
},
"Toolbar": {
"DownloadCsv": "Download CSV",
"FilterTable": "Filter table",
"Print": "Print",
"Search": "Search",
"ViewColumns": "View columns"
},
"ViewColumns": {
"Title": "Show columns"
}
},
"Edit": "Edit",
"More": "More",
"OpenInNewTab": "Open in new tab"
},
"Menu": { "Menu": {
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Resources": "Resources", "Resources": "Resources",
@ -37,5 +68,24 @@
"IncorrectCredentials": "Incorrect credentials.", "IncorrectCredentials": "Incorrect credentials.",
"Hello": "Hi, {{username}}", "Hello": "Hi, {{username}}",
"AuthenticationDate": "Authentication date" "AuthenticationDate": "Authentication date"
},
"Resource": {
"Code": "Code",
"Name": "Name",
"Category": "Category",
"Secured": "Secured",
"List": {
"Title": "Resource management",
"SubTitle": "Resources",
"Filters": {
"Code": "Code contains: {{value}}",
"Name": "Name contains: {{value}}",
"Category": "Category: {{value}}"
},
"Actions": {
"CopyUrl": "Copy resource URL",
"LinkCopiedToClipboard": "The link has been copied to the clipboard."
}
}
} }
} }

View File

@ -3,6 +3,37 @@
"English": "Engleză", "English": "Engleză",
"Romanian": "Română" "Romanian": "Română"
}, },
"Generic": {
"Table": {
"Body": {
"NoMatch": "Ne pare rău, nu s-au găsit înregistrări care se potrivesc"
},
"Filter": {
"All": "Toate",
"Reset": "Resetați",
"Title": "FILTRE"
},
"Pagination": {
"DisplayRows": "din",
"Next": "Pagina următoare",
"Previous": "Pagina anterioară",
"RowsPerPage": "Rânduri pe pagină"
},
"Toolbar": {
"DownloadCsv": "Descărcați CSV",
"FilterTable": "Filtrează tabel",
"Print": "Imprimare",
"Search": "Căutare",
"ViewColumns": "Vizualizați coloanele"
},
"ViewColumns": {
"Title": "Afișați coloanele"
}
},
"Edit": "Editează",
"More": "Mai mult",
"OpenInNewTab": "Deschide într-un tab nou"
},
"Menu": { "Menu": {
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Resources": "Resurse", "Resources": "Resurse",
@ -28,5 +59,24 @@
"IncorrectCredentials": "Credențiale incorecte.", "IncorrectCredentials": "Credențiale incorecte.",
"Hello": "Salut, {{username}}", "Hello": "Salut, {{username}}",
"AuthenticationDate": "Momentul autentificării" "AuthenticationDate": "Momentul autentificării"
},
"Resource": {
"Code": "Cod",
"Name": "Nume",
"Category": "Categorie",
"Secured": "Securizat",
"List": {
"Title": "Managementul resurselor",
"SubTitle": "Resurse",
"Filters": {
"Code": "Codul conține: {{value}}",
"Name": "Numele conține: {{value}}",
"Category": "Categorie: {{value}}"
},
"Actions": {
"CopyUrl": "Copiați adresa URL a resursei",
"LinkCopiedToClipboard": "Linkul a fost copiat în clipboard."
}
}
} }
} }

View File

@ -4,7 +4,8 @@ import { get } from "../utils/axios";
const cdn = process.env.REACT_APP_CDN_URL; const cdn = process.env.REACT_APP_CDN_URL;
const endpoints = { const endpoints = {
mimeTypes: `${cdn}/admin/mime-types` mimeTypes: `${cdn}/admin/mime-types`,
resourceCategories: `${cdn}/admin/resource-categories`
}; };
const useDictionariesApi = () => { const useDictionariesApi = () => {
@ -18,8 +19,17 @@ const useDictionariesApi = () => {
[exec] [exec]
); );
const getResourceCategories = useCallback(
(options) => {
const promise = exec(() => get(endpoints.resourceCategories), options);
return promise;
},
[exec]
);
return { return {
getMimeTypes getMimeTypes,
getResourceCategories
}; };
}; };

View File

@ -0,0 +1,48 @@
import React from "react";
import PropTypes from "prop-types";
import grey from "@material-ui/core/colors/grey";
import { Paper, makeStyles } from "@material-ui/core";
const styles = {
cover: {
width: "100%"
},
bar: {
backgroundColor: grey[200],
height: 10,
margin: 20,
"&:nth-child(2n)": {
marginRight: "20%"
}
},
paper: {
padding: "10px",
borderRadius: "6px"
}
};
const useStyles = makeStyles(styles);
const LoadingText = ({ lines = 4, onPaper = false, ...props }) => {
const classes = useStyles();
const loadingText = (
<div className={classes.cover} {...props}>
{[...Array(lines)].map((_e, i) => (
<div className={classes.bar} key={i}></div>
))}
</div>
);
if (onPaper) {
return <Paper className={classes.paper}>{loadingText}</Paper>;
}
return loadingText;
};
LoadingText.propTypes = {
lines: PropTypes.number,
onPaper: PropTypes.bool
};
export default LoadingText;

3
src/components/index.js Normal file
View File

@ -0,0 +1,3 @@
import LoadingText from "./LoadingContent/LoadingText";
export { LoadingText };

View File

@ -1,6 +1,7 @@
const defaultResourcesFilters = Object.freeze({ const defaultResourcesFilters = Object.freeze({
page: 1, page: 1,
pageSize: 10, pageSize: 10,
loadedPages: null,
sortBy: null, sortBy: null,
sortDirection: null, sortDirection: null,
fullTextSearch: null, fullTextSearch: null,

View File

@ -0,0 +1,24 @@
import React, { forwardRef } from "react";
import { IconButton, Tooltip } from "@material-ui/core";
const ActionButton = forwardRef((props, _ref) => {
const { action, resource } = props;
return (
<Tooltip
id={`resource-${resource.resourceId}-${action.code}-tooltip`}
title={action.tooltip}
>
<span id={`resource-${resource.resourceId}-${action.code}`}>
<IconButton
id={`resource-${resource.resourceId}-${action.code}`}
size={"small"}
onClick={(event) => action.effect(event, resource)}
>
<action.icon />
</IconButton>
</span>
</Tooltip>
);
});
export default ActionButton;

View File

@ -1,49 +1,122 @@
import React, { useState, useEffect, useMemo, useCallback } from "react"; import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Grid, Checkbox, CircularProgress, FormLabel } from "@material-ui/core"; import { Checkbox, FormLabel } from "@material-ui/core";
import MUIDataTable, { debounceSearchRender } from "mui-datatables"; import MUIDataTable, { debounceSearchRender } from "mui-datatables";
import { LoadingText } from "../../../components";
import PageTitle from "../../../components/PageTitle"; import PageTitle from "../../../components/PageTitle";
import { useResourcesApi } from "../../../api"; import { useResourcesApi, useDictionariesApi } from "../../../api";
import { defaultResourcesFilters } from "../../../constants/resourcesConstants"; import { defaultResourcesFilters } from "../../../constants/resourcesConstants";
import { useTranslation } from "react-i18next";
import ActionButton from "./ActionButton";
import SecondaryActionsGroup from "./SecondaryActionsGroup";
import {
EditOutlined,
FileCopyOutlined,
OpenInNewOutlined
} from "@material-ui/icons";
import { useToast } from "../../../context/ToastContext";
const __ROWS_PER_PAGE_OPTIONS = [10, 20, 50, 100]; const __ROWS_PER_PAGE_OPTIONS = [10, 20, 50, 100];
const __RESOURCE_NAME_MAX_LENGTH = 35;
const ResourcesContainer = () => { const ResourcesContainer = () => {
const [state, setState] = useState({ loaded: false, loadedPages: null }); const [state, setState] = useState({
pageSize: 10,
totalCount: 0,
values: []
});
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({ ...defaultResourcesFilters }); const [filters, setFilters] = useState({ ...defaultResourcesFilters });
const [resourceCategories, setResourceCategories] = useState([]);
const { t } = useTranslation();
const { info } = useToast();
const { getResources } = useResourcesApi(); const { getResources } = useResourcesApi();
const { getResourceCategories } = useDictionariesApi();
useEffect(() => { useEffect(() => {
if (state.loadedPages && state.loadedPages.includes(filters.page)) return; getResourceCategories().then((r) => setResourceCategories(r));
}, [getResourceCategories]);
useEffect(() => {
if (filters.loadedPages && filters.loadedPages.includes(filters.page))
return;
setLoading(true);
getResources(filters, { getResources(filters, {
onCompleted: (resources) => { onCompleted: (resources) => {
const data = { ...resources, loaded: true };
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
...data, ...resources,
values: prev?.values ? [...prev.values, ...data.values] : data.values, values: filters.loadedPages
loadedPages: prev?.loadedPages ? [...prev.values, ...resources.values]
? [...prev.loadedPages, data.page] : resources.values
: [data.page]
})); }));
setFilters((prev) => ({
...prev,
loadedPages: prev?.loadedPages
? [...prev.loadedPages, resources.page]
: [resources.page]
}));
setLoading(false);
} }
}); });
}, [getResources, filters, state.loadedPages]); }, [getResources, filters]);
const categoryFilterOptions = useMemo(
() => resourceCategories.map((z) => z.categoryName),
[resourceCategories]
);
const actions = useMemo(
() => [
{
code: "edit",
effect: () => alert("edit"),
icon: EditOutlined,
tooltip: t("Generic.Edit"),
top: true
},
{
code: "copy-url",
effect: (_event, resource) => {
navigator.clipboard.writeText(resource.url);
info(t("Resource.List.Actions.LinkCopiedToClipboard"));
},
icon: FileCopyOutlined,
tooltip: t("Resource.List.Actions.CopyUrl"),
top: true
},
{
code: "open-in-new-tab",
effect: (event, resource) => {
window.open(resource.url, "_blank");
event.preventDefault();
},
icon: OpenInNewOutlined,
tooltip: t("Generic.OpenInNewTab"),
top: false
}
],
[t, info]
);
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
label: "Code", label: t("Resource.Code"),
name: "resourceCode", name: "resourceCode",
options: { options: {
display: true, display: true,
draggable: true, draggable: true,
download: false, download: true,
print: false, print: false,
searchable: true, searchable: true,
sort: true, sort: true,
customFilterListOptions: { customFilterListOptions: {
render: (v) => (v ? `Code contains: ${v}` : []) render: (v) =>
v ? t("Resource.List.Filters.Code", { value: v }) : []
}, },
filter: true, filter: true,
filterType: "textField", filterType: "textField",
@ -55,17 +128,27 @@ const ResourcesContainer = () => {
} }
}, },
{ {
label: "Name", label: t("Resource.Name"),
name: "resourceName", name: "resourceName",
options: { options: {
display: true, display: true,
draggable: true, draggable: true,
download: false, download: true,
print: false, print: false,
searchable: true, searchable: true,
sort: true, sort: true,
customBodyRenderLite: (dataIndex, _rowIndex) => {
if (loading) return;
const value = state.values[dataIndex].resourceName;
const formattedValue =
value.length > __RESOURCE_NAME_MAX_LENGTH
? `${value.substring(0, __RESOURCE_NAME_MAX_LENGTH + 1)}[...]`
: value;
return formattedValue;
},
customFilterListOptions: { customFilterListOptions: {
render: (v) => (v ? `Name contains: ${v}` : []) render: (v) =>
v ? t("Resource.List.Filters.Name", { value: v }) : []
}, },
filter: true, filter: true,
filterType: "textField", filterType: "textField",
@ -77,31 +160,34 @@ const ResourcesContainer = () => {
} }
}, },
{ {
label: "Category", label: t("Resource.Category"),
name: "categoryId", name: "categoryName",
options: { options: {
display: true, display: true,
draggable: true, draggable: true,
download: false, download: true,
print: false, print: false,
searchable: true, searchable: true,
sort: true, sort: true,
filter: true, filter: true,
customFilterListOptions: {
render: (v) => t("Resource.List.Filters.Category", { value: v })
},
filterType: "dropdown", filterType: "dropdown",
filterOptions: { filterOptions: {
names: categoryFilterOptions,
logic: (_prop, _filterValue, _row) => false logic: (_prop, _filterValue, _row) => false
}, },
filterList: [],
hint: undefined hint: undefined
} }
}, },
{ {
label: "Secured", label: t("Resource.Secured"),
name: "secured", name: "secured",
options: { options: {
display: true, display: true,
draggable: true, draggable: true,
download: false, download: true,
print: false, print: false,
searchable: true, searchable: true,
sort: true, sort: true,
@ -118,7 +204,7 @@ const ResourcesContainer = () => {
customFilterListOptions: { customFilterListOptions: {
render: (v) => { render: (v) => {
const checked = v[0]; const checked = v[0];
return checked ? <span>Secured</span> : []; return checked ? <span>{t("Resource.Secured")}</span> : [];
} }
}, },
filter: true, filter: true,
@ -128,7 +214,7 @@ const ResourcesContainer = () => {
display: (filterList, onChange, index, column) => { display: (filterList, onChange, index, column) => {
return ( return (
<div> <div>
<FormLabel>Secured</FormLabel> <FormLabel>{t("Resource.Secured")}</FormLabel>
<Checkbox <Checkbox
color="primary" color="primary"
checked={filterList[index][0] || false} checked={filterList[index][0] || false}
@ -141,78 +227,165 @@ const ResourcesContainer = () => {
); );
} }
}, },
filterList: undefined,
hint: undefined hint: undefined
} }
},
{
label: "",
name: "actions",
options: {
customHeadLabelRender: (_options) => "",
display: true,
draggable: false,
download: false,
print: false,
searchable: false,
sort: false,
customBodyRenderLite: (dataIndex, _rowIndex) => {
if (loading) return;
const resource = state.values[dataIndex];
return (
<>
{actions
.filter((a) => a.top === true)
.map((action) => (
<ActionButton
key={`resource-${resource.resourceId}-${action.code}`}
action={action}
resource={resource}
/>
))}
<SecondaryActionsGroup
resource={resource}
actions={actions.filter((a) => a.top === false)}
/>
</>
);
},
setCellProps: (_cellValue, _rowIndex, _columnIndex) => ({
style: {
paddingTop: "0px",
paddingBottom: "0px",
textAlign: "right"
}
}),
filter: false
}
} }
], ],
[] [loading, state.values, categoryFilterOptions, t, actions]
); );
const handleResetFilters = useCallback(() => { const changeFilters = useCallback((obj) => {
setFilters((prev) => ({ ...prev, ...defaultResourcesFilters })); const keys = Object.keys(obj);
}, [setFilters]); const isPageChange = keys.length === 1 && keys[0] === "page";
if (!isPageChange) {
const { page, loadedPages } = defaultResourcesFilters;
obj = { ...obj, page, loadedPages };
}
setFilters((prev) => ({ ...prev, ...obj }));
}, []);
const getCategoryId = useCallback(
(name) => {
const category = resourceCategories.find((z) => z.categoryName === name);
return category?.categoryId;
},
[resourceCategories]
);
const handleFilterChange = useCallback( const handleFilterChange = useCallback(
(changedColumn, filterList, type, changedColumnIndex, displayData) => { (changedColumn, filterList, type, changedColumnIndex, _displayData) => {
if (type === "reset") { if (type === "reset") {
handleResetFilters(); changeFilters(defaultResourcesFilters);
return; return;
} }
const filterValue = filterList[changedColumnIndex][0]; const filterValue = filterList[changedColumnIndex][0];
setFilters((prev) => ({
...prev, if (changedColumn === "categoryName") {
[changedColumn]: filterValue const categoryId = filterValue
})); ? getCategoryId(filterValue)
: filterValue;
changeFilters({ categoryId });
return;
}
changeFilters({ [changedColumn]: filterValue });
}, },
[handleResetFilters] [changeFilters, getCategoryId]
); );
return ( return (
<> <>
<PageTitle title="ResourcesX" /> <PageTitle title={t("Resource.List.Title")} />
{state.loaded === false ? (
<CircularProgress size={26} /> <MUIDataTable
) : ( title={t("Resource.List.SubTitle")}
<MUIDataTable columns={columns}
title="Resources" data={state.values}
columns={columns} options={{
data={state.values ?? []} filterType: "textField",
options={{ expandableRows: false,
filterType: "textField", print: false,
expandableRows: false, selectableRows: "none",
print: false, page: filters.page - 1,
selectableRows: "none", rowsPerPage: state.pageSize,
rowsPerPage: state.pageSize, rowsPerPageOptions: __ROWS_PER_PAGE_OPTIONS,
rowsPerPageOptions: __ROWS_PER_PAGE_OPTIONS, count: state.totalCount,
count: state.totalCount, customSearchRender: debounceSearchRender(500),
customSearchRender: debounceSearchRender(500), onChangePage: (currentPage) =>
onChangePage: (currentPage) => { changeFilters({ page: currentPage + 1 }),
setFilters((prev) => ({ ...prev, page: currentPage + 1 })); onChangeRowsPerPage: (numberOfRows) =>
changeFilters({ pageSize: numberOfRows }),
onColumnSortChange: (changedColumn, direction) =>
changeFilters({
sortBy: changedColumn,
sortDirection: direction
}),
onFilterChange: handleFilterChange,
onSearchChange: (text) => changeFilters({ fullTextSearch: text }),
customSort: (data) => data,
customSearch: () => true,
setFilterChipProps: (_colIndex, _colName, _data) => {
return {
color: "primary",
variant: "outlined"
};
},
textLabels: {
body: {
noMatch: loading ? (
<LoadingText lines={15} />
) : (
<>{t("Generic.Table.Body.NoMatch")}</>
)
}, },
onChangeRowsPerPage: (numberOfRows) => { filter: {
setFilters((prev) => ({ ...prev, pageSize: numberOfRows })); all: t("Generic.Table.Filter.All"),
reset: t("Generic.Table.Filter.Reset"),
title: t("Generic.Table.Filter.Title")
}, },
onColumnSortChange: (changedColumn, direction) => pagination: {
setFilters((prev) => ({ displayRows: t("Generic.Table.Pagination.DisplayRows"),
...prev, next: t("Generic.Table.Pagination.Next"),
sortBy: changedColumn, previous: t("Generic.Table.Pagination.Previous"),
sortDirection: direction rowsPerPage: t("Generic.Table.Pagination.RowsPerPage")
})), },
onFilterChange: handleFilterChange, toolbar: {
onSearchChange: (text) => downloadCsv: t("Generic.Table.Toolbar.DownloadCsv"),
setFilters((prev) => ({ ...prev, fullTextSearch: text })), filterTable: t("Generic.Table.Toolbar.FilterTable"),
customSort: (data) => data, print: t("Generic.Table.Toolbar.Print"),
customSearch: () => true, search: t("Generic.Table.Toolbar.Search"),
setFilterChipProps: (_colIndex, _colName, _data) => { viewColumns: t("Generic.Table.Toolbar.ViewColumns")
return { },
color: "primary", viewColumns: {
variant: "outlined" title: t("Generic.Table.ViewColumns.Title")
};
} }
}} }
/> }}
)} />
</> </>
); );
}; };

View File

@ -0,0 +1,43 @@
import React, { useState } from "react";
import { Menu } from "@material-ui/core";
import ActionButton from "./ActionButton";
import { MoreHorizOutlined } from "@material-ui/icons";
import { useTranslation } from "react-i18next";
const SecondaryActionsGroup = ({ resource, actions }) => {
const [menuAnchor, setMenuAnchor] = useState(null);
const { t } = useTranslation();
return (
<>
<ActionButton
key={`resource-${resource.resourceId}-more`}
action={{
code: "more",
effect: (event) => setMenuAnchor(event.currentTarget),
icon: MoreHorizOutlined,
tooltip: t("Generic.More"),
top: true
}}
resource={resource}
/>
<Menu
id={`resource-${resource.resourceId}-secondary-actions-menu`}
key={`resource-${resource.resourceId}-secondary-actions-menu`}
anchorEl={menuAnchor}
open={Boolean(menuAnchor)}
onClose={() => setMenuAnchor(null)}
>
{actions.map((action) => (
<ActionButton
key={`resource-${resource.resourceId}-${action.code}`}
action={action}
resource={resource}
/>
))}
</Menu>
</>
);
};
export default SecondaryActionsGroup;