Merge pull request #312 from ONLYOFFICE/feature/table-view

Feature/table view
This commit is contained in:
Ilya Oleshko 2021-08-27 16:26:29 +03:00 committed by GitHub
commit 43ee204e80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 3546 additions and 1137 deletions

View File

@ -161,7 +161,9 @@ export function getFoldersTree() {
// : null,
folders: null,
pathParts: data.pathParts,
foldersCount: !isRecycleBinFolder ? data.current.foldersCount : null,
foldersCount: !isRecycleBinFolder
? data.current.foldersCount || data.folders.length
: null,
newItems: data.new,
};
});

View File

@ -832,6 +832,7 @@ class FilterInput extends React.Component {
isMobile,
sectionWidth,
getViewSettingsData,
viewSelectorVisible,
} = this.props;
/* eslint-enable react/prop-types */
@ -911,31 +912,33 @@ class FilterInput extends React.Component {
</SearchInput>
</div>
<div ref={this.rectComboBoxRef}>
<SortComboBox
options={getSortData()}
viewSettings={this.state.viewSettings}
isDisabled={isDisabled}
onChangeSortId={this.onClickSortItem}
onChangeView={this.props.onChangeViewAs}
onChangeSortDirection={this.onChangeSortDirection}
selectedOption={
getSortData().length > 0
? getSortData().find((x) => x.key === sortId)
: {}
}
onButtonClick={this.onSortDirectionClick}
viewAs={viewAs}
sortDirection={+sortDirection}
directionAscLabel={directionAscLabel}
directionDescLabel={directionDescLabel}
/>
{viewAs !== "table" && (
<SortComboBox
options={getSortData()}
viewSettings={this.state.viewSettings}
isDisabled={isDisabled}
onChangeSortId={this.onClickSortItem}
onChangeView={this.props.onChangeViewAs}
onChangeSortDirection={this.onChangeSortDirection}
selectedOption={
getSortData().length > 0
? getSortData().find((x) => x.key === sortId)
: {}
}
onButtonClick={this.onSortDirectionClick}
viewAs={viewAs}
sortDirection={+sortDirection}
directionAscLabel={directionAscLabel}
directionDescLabel={directionDescLabel}
/>
)}
</div>
{viewAs && !isMobileOnly && (
{viewAs && !isMobileOnly && viewSelectorVisible && (
<ViewSelector
className="view-selector-button"
className="view-selector-button not-selectable"
isDisabled={isDisabled}
onChangeView={this.props.onChangeViewAs}
viewAs={viewAs}
viewAs={viewAs === "table" ? "row" : viewAs}
viewSettings={this.state.viewSettings}
/>
)}
@ -976,6 +979,7 @@ FilterInput.defaultProps = {
needForUpdate: false,
directionAscLabel: "A-Z",
directionDescLabel: "Z-A",
viewSelectorVisible: true,
};
export default FilterInput;

View File

@ -166,7 +166,11 @@ class PageLayout extends React.Component {
(x) => x.classList && x.classList.contains("not-selectable")
);
if (notSelectablePath || isBackdrop) {
const isDraggable = path.some(
(x) => x.classList && x.classList.contains("draggable")
);
if (notSelectablePath || isBackdrop || isDraggable) {
return false;
} else return true;
};
@ -191,7 +195,6 @@ class PageLayout extends React.Component {
//withBodyAutoFocus,
withBodyScroll,
children,
isLoaded,
isHeaderVisible,
//headerBorderBottom,
onOpenUploadPanel,
@ -277,7 +280,6 @@ class PageLayout extends React.Component {
<Article
visible={isArticleVisible}
pinned={isArticlePinned}
isLoaded={isLoaded}
firstLoad={firstLoad}
>
{isArticleHeaderAvailable && (
@ -489,7 +491,6 @@ PageLayout.propTypes = {
setSelections: PropTypes.func,
uploadFiles: PropTypes.bool,
hideAside: PropTypes.bool,
isLoaded: PropTypes.bool,
viewAs: PropTypes.string,
uploadPanelVisible: PropTypes.bool,
onOpenUploadPanel: PropTypes.func,

View File

@ -56,6 +56,7 @@ class Checkbox extends React.Component {
}
onInputChange(e) {
e.stopPropagation();
this.setState({ checked: e.target.checked });
this.props.onChange && this.props.onChange(e);
}

View File

@ -12,9 +12,19 @@ const DragAndDrop = (props) => {
acceptedFiles.length && props.onDrop && props.onDrop(acceptedFiles);
};
const onDragOver = (e) => {
props.onDragOver && props.onDragOver(isDragActive, e);
};
const onDragLeave = (e) => {
props.onDragLeave && props.onDragLeave(e);
};
const { getRootProps, isDragActive } = useDropzone({
noDragEventsBubbling: !isDropZone,
onDrop,
onDragOver,
onDragLeave,
});
return (
@ -44,6 +54,8 @@ DragAndDrop.propTypes = {
onMouseDown: PropTypes.func,
/** Occurs when the dragged element is dropped on the drop target */
onDrop: PropTypes.func,
onDragOver: PropTypes.func,
onDragLeave: PropTypes.func,
};
export default DragAndDrop;

View File

@ -62,3 +62,4 @@ export { default as DragAndDrop } from "./drag-and-drop";
export { default as ViewSelector } from "./view-selector";
export * as Themes from "./themes";
export { default as Portal } from "./portal";
export { default as TableContainer } from "./TableContainer";

View File

@ -1,5 +1,6 @@
import React from "react";
import PropTypes from "prop-types";
import { isMobile } from "react-device-detect";
import {
TabletSideInfo,
@ -46,7 +47,6 @@ const RowContent = (props) => {
sideColor,
onClick,
sectionWidth,
isMobile,
convertSideInfo,
} = props;

View File

@ -0,0 +1,237 @@
import styled, { css } from "styled-components";
import Base from "../themes/base";
const HeaderStyles = css`
height: 39px;
position: fixed;
background: #fff;
z-index: 1;
border-bottom: 1px solid #eceef1;
`;
const StyledTableContainer = styled.div`
width: 100%;
max-width: 100%;
margin-top: -18px;
display: grid;
.table-column {
user-select: none;
position: relative;
min-width: 10%;
}
.resize-handle {
display: block;
cursor: ew-resize;
height: 10px;
margin: 14px 8px 0 auto;
z-index: 1;
border-right: 2px solid #d0d5da;
}
.content-container {
overflow: hidden;
}
.children-wrap {
display: flex;
flex-direction: column;
}
.table-cell {
height: 47px;
border-bottom: 1px solid #eceef1;
}
.table-container_group-menu {
.table-container_group-menu-checkbox {
width: 22px;
}
.table-container_group-menu-combobox {
height: 24px;
width: 16px;
margin-bottom: 16px;
.combo-button {
height: 24px;
margin-top: 8px;
width: 16px;
.combo-buttons_arrow-icon {
margin: 8px 16px 0 0;
/* svg {
path {
fill: #333;
}
} */
}
}
}
.table-container_group-menu-separator {
border-right: 1px solid #eceef1;
width: 2px;
height: 10px;
margin: 0 8px;
}
}
`;
const StyledTableGroupMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
width: ${(props) => props.width};
${HeaderStyles}
.table-container_group-menu-checkbox {
${(props) => props.checkboxMargin && `margin-left: ${props.checkboxMargin}`}
}
.table-container_group-menu_button {
margin-right: 8px;
}
`;
const StyledTableHeader = styled.div`
display: grid;
${HeaderStyles}
.table-container_header-checkbox {
${(props) => props.checkboxMargin && `margin-left: ${props.checkboxMargin}`}
}
.table-container_header-cell {
overflow: hidden;
}
`;
const StyledTableHeaderCell = styled.div`
.table-container_header-item {
display: flex;
user-select: none;
}
.header-container-text-wrapper {
display: flex;
cursor: pointer;
.header-container-text-icon {
padding: 16px 0 0 4px;
display: ${(props) => (props.isActive ? "block" : "none")};
${(props) =>
props.sorted &&
css`
transform: scale(1, -1);
padding: 14px 0 0 4px;
`}
}
:hover {
.header-container-text-icon {
display: block;
}
}
}
.header-container-text {
height: 38px;
display: flex;
align-items: center;
}
`;
const StyledTableBody = styled.div`
display: contents;
`;
const StyledTableRow = styled.div`
display: contents;
.table-container_header-checkbox {
svg {
margin: 0;
}
}
.droppable-hover {
background: ${(props) =>
props.dragging
? `${props.theme.dragAndDrop.acceptBackground} !important`
: "none"};
}
`;
const StyledTableCell = styled.div`
/* padding-right: 8px; */
height: 40px;
max-height: 40px;
border-bottom: 1px solid #eceef1;
overflow: hidden;
display: flex;
align-items: center;
.react-svg-icon svg {
margin-top: 2px;
}
.table-container_element {
display: ${(props) => (props.checked ? "none" : "flex")};
}
.table-container_row-checkbox {
display: ${(props) => (props.checked ? "flex" : "none")};
}
${(props) =>
props.hasAccess &&
css`
:hover {
.table-container_element {
display: none;
}
.table-container_row-checkbox {
display: flex;
}
}
`}
`;
const StyledTableSettings = styled.div`
margin: 14px 0 0px 8px;
display: inline-block;
position: relative;
cursor: pointer;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
.table-container_settings-checkbox {
padding: 8px 16px;
}
`;
const StyledEmptyTableContainer = styled.div`
grid-column-start: 1;
grid-column-end: -1;
height: 40px;
`;
StyledTableRow.defaultProps = { theme: Base };
export {
StyledTableContainer,
StyledTableRow,
StyledTableBody,
StyledTableHeader,
StyledTableHeaderCell,
StyledTableCell,
StyledTableSettings,
StyledTableGroupMenu,
StyledEmptyTableContainer,
};

View File

@ -0,0 +1,8 @@
import React from "react";
import { StyledTableBody } from "./StyledTableContainer";
const TableBody = (props) => {
return <StyledTableBody className="table-container_body" {...props} />;
};
export default TableBody;

View File

@ -0,0 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import { StyledTableCell } from "./StyledTableContainer";
const TableCell = ({ className, forwardedRef, ...rest }) => {
return (
<StyledTableCell
className={`${className} table-container_cell`}
ref={forwardedRef}
{...rest}
/>
);
};
TableCell.propTypes = {
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
forwardedRef: PropTypes.shape({ current: PropTypes.any }),
};
export default TableCell;

View File

@ -0,0 +1,20 @@
import React from "react";
import { StyledTableContainer } from "./StyledTableContainer";
import PropTypes from "prop-types";
const TableContainer = (props) => {
return (
<StyledTableContainer
id="table-container"
className="table-container"
ref={props.forwardedRef}
{...props}
/>
);
};
TableContainer.propTypes = {
forwardedRef: PropTypes.shape({ current: PropTypes.any }),
};
export default TableContainer;

View File

@ -0,0 +1,85 @@
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import Checkbox from "../checkbox";
import { StyledTableGroupMenu } from "./StyledTableContainer";
import Button from "../button";
import ComboBox from "../combobox";
const TableGroupMenu = (props) => {
const {
isChecked,
isIndeterminate,
headerMenu,
containerRef,
onChange,
checkboxOptions,
columnStorageName,
checkboxMargin,
...rest
} = props;
const onCheckboxChange = (e) => {
onChange && onChange(e.target && e.target.checked);
};
const width = containerRef.current
? containerRef.current.clientWidth + "px"
: "100%";
useEffect(() => {
const storageSize = localStorage.getItem(columnStorageName);
if (containerRef.current)
containerRef.current.style.gridTemplateColumns = storageSize;
}, [containerRef]);
return (
<>
<StyledTableGroupMenu
width={width}
className="table-container_group-menu"
checkboxMargin={checkboxMargin}
{...rest}
>
<Checkbox
className="table-container_group-menu-checkbox"
onChange={onCheckboxChange}
isChecked={isChecked}
isIndeterminate={isIndeterminate}
/>
<ComboBox
advancedOptions={checkboxOptions}
className="table-container_group-menu-combobox not-selectable"
options={[]}
selectedOption={{}}
/>
<div className="table-container_group-menu-separator" />
{headerMenu.map((item, index) => {
const { label, disabled, onClick } = item;
return (
<Button
key={index}
className="table-container_group-menu_button not-selectable"
isDisabled={disabled}
onClick={onClick}
label={label}
/>
);
})}
</StyledTableGroupMenu>
</>
);
};
TableGroupMenu.propTypes = {
isChecked: PropTypes.bool,
isIndeterminate: PropTypes.bool,
headerMenu: PropTypes.arrayOf(PropTypes.object),
checkboxOptions: PropTypes.any.isRequired,
onClick: PropTypes.func,
onChange: PropTypes.func,
containerRef: PropTypes.shape({ current: PropTypes.any }),
columnStorageName: PropTypes.string,
checkboxMargin: PropTypes.string,
};
export default TableGroupMenu;

View File

@ -0,0 +1,417 @@
import React from "react";
import PropTypes from "prop-types";
import throttle from "lodash.throttle";
import {
StyledTableHeader,
StyledTableRow,
StyledEmptyTableContainer,
} from "./StyledTableContainer";
import Checkbox from "../checkbox";
import TableSettings from "./TableSettings";
import TableHeaderCell from "./TableHeaderCell";
import { size } from "../utils/device";
import TableGroupMenu from "./TableGroupMenu";
const minColumnSize = 90;
const settingsSize = 24;
class TableHeader extends React.Component {
constructor(props) {
super(props);
this.state = { columnIndex: null };
this.headerRef = React.createRef();
this.throttledResize = throttle(this.onResize, 300);
}
componentDidMount() {
this.onResize();
window.addEventListener("resize", this.throttledResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.throttledResize);
}
componentDidUpdate() {
if (this.props.sectionWidth >= size.tablet + 24) {
this.onResize();
}
}
getSubstring = (str) => +str.substring(0, str.length - 2);
getNextColumn = (array, index) => {
let i = 1;
while (array.length !== i) {
const item = array[index + i];
if (!item) return null;
else if (!item.enable) i++;
else return item;
}
};
getColumn = (array, index) => {
let i = 1;
while (array.length !== i) {
const item = array[index + i];
if (!item) return [0, i];
else if (item === "0px") i++;
else return [this.getSubstring(item), i];
}
};
moveToLeft = (widths, newWidth, index) => {
const { columnIndex } = this.state;
let leftColumn;
let colIndex = index ? index : columnIndex - 1;
if (colIndex === 0) return;
while (colIndex !== 0) {
leftColumn = document.getElementById("column_" + colIndex);
if (leftColumn) {
if (leftColumn.dataset.enable === "true") break;
else colIndex--;
} else return false;
}
const minSize = leftColumn.dataset.minWidth
? leftColumn.dataset.minWidth
: minColumnSize;
if (leftColumn.clientWidth <= minSize) {
if (colIndex === 1) return false;
return this.moveToLeft(widths, newWidth, colIndex - 1);
}
const offset = this.getSubstring(widths[+columnIndex]) - newWidth;
const column2Width = this.getSubstring(widths[colIndex]);
const leftColumnWidth = column2Width - offset;
const newLeftWidth = leftColumnWidth < minSize ? minSize : leftColumnWidth;
widths[colIndex] = newLeftWidth + "px";
widths[+columnIndex] =
this.getSubstring(widths[+columnIndex]) +
(offset - (newLeftWidth - leftColumnWidth)) +
"px";
};
moveToRight = (widths, newWidth, index) => {
const { columnIndex } = this.state;
let rightColumn;
let colIndex = index ? index : +columnIndex + 1;
while (colIndex !== this.props.columns.length) {
rightColumn = document.getElementById("column_" + colIndex);
if (rightColumn) {
if (rightColumn.dataset.enable === "true") break;
else colIndex++;
} else return false;
}
const offset = this.getSubstring(widths[+columnIndex]) - newWidth;
const column2Width = this.getSubstring(widths[colIndex]);
if (column2Width + offset >= minColumnSize) {
widths[+columnIndex] = newWidth + "px";
widths[colIndex] = column2Width + offset + "px";
} else {
if (colIndex === this.props.columns.length) return false;
return this.moveToRight(widths, newWidth, colIndex + 1);
}
};
addNewColumns = (gridTemplateColumns, columnIndex) => {
const filterColumns = this.props.columns
.filter((x) => x.enable)
.filter((x) => x.key !== this.props.columns[columnIndex - 1].key)
.filter((x) => !x.defaultSize);
let index = this.props.columns.length;
while (index !== 0) {
index--;
const someItem = this.props.columns[index];
const isFind = filterColumns.find((x) => x.key === someItem.key);
if (isFind) {
const someItemById = document.getElementById("column_" + (index + 1));
const columnSize = someItemById.clientWidth - minColumnSize;
if (columnSize >= minColumnSize) {
return (gridTemplateColumns[index + 1] = columnSize + "px");
}
}
}
};
onMouseMove = (e) => {
const { columnIndex } = this.state;
const { containerRef } = this.props;
if (!columnIndex) return;
const column = document.getElementById("column_" + columnIndex);
const columnSize = column.getBoundingClientRect();
const newWidth = e.clientX - columnSize.left;
const tableContainer = containerRef.current.style.gridTemplateColumns;
const widths = tableContainer.split(" ");
const minSize = column.dataset.minWidth
? column.dataset.minWidth
: minColumnSize;
if (newWidth <= minSize) {
const columnChanged = this.moveToLeft(widths, newWidth);
if (!columnChanged) {
widths[+columnIndex] = widths[+columnIndex];
}
} else {
this.moveToRight(widths, newWidth);
}
containerRef.current.style.gridTemplateColumns = widths.join(" ");
this.headerRef.current.style.gridTemplateColumns = widths.join(" ");
};
onMouseUp = () => {
localStorage.setItem(
this.props.columnStorageName,
this.props.containerRef.current.style.gridTemplateColumns
);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
};
onMouseDown = (event) => {
this.setState({ columnIndex: event.target.dataset.column });
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
};
onResize = () => {
const { containerRef, columnStorageName, checkboxSize } = this.props;
let activeColumnIndex = null;
const container = containerRef.current
? containerRef.current
: document.getElementById("table-container");
if (!container) return;
const storageSize = localStorage.getItem(columnStorageName);
const tableContainer = storageSize
? storageSize.split(" ")
: container.style.gridTemplateColumns.split(" ");
const containerWidth = +container.clientWidth;
const newContainerWidth =
containerWidth - this.getSubstring(checkboxSize) - 80 - settingsSize; // TODO: 80
const oldWidth = tableContainer
.map((column) => this.getSubstring(column))
.reduce((x, y) => x + y);
const enableColumns = this.props.columns
.filter((x) => !x.default)
.filter((x) => x.enable)
.filter((x) => !x.defaultSize);
const isSingleTable = enableColumns.length > 0;
let str = "";
let disableColumnWidth = 0;
if (tableContainer.length > 1) {
const gridTemplateColumns = [];
for (let index in tableContainer) {
const item = tableContainer[index];
const column = document.getElementById("column_" + index);
const enable =
index == 0 ||
index == tableContainer.length - 1 ||
(column ? column.dataset.enable === "true" : item !== "0px");
const isActiveNow = item === "0px" && enable;
if (isActiveNow && column) activeColumnIndex = index;
if (!enable) {
gridTemplateColumns.push("0px");
gridTemplateColumns[1] =
this.getSubstring(gridTemplateColumns[1]) +
this.getSubstring(item) +
"px";
} else if (
item !== `${settingsSize}px` &&
item !== checkboxSize &&
item !== "80px"
) {
const percent = (this.getSubstring(item) / oldWidth) * 100;
if (index == 1) {
const newItemWidth =
(containerWidth * percent) / 100 + disableColumnWidth + "px";
gridTemplateColumns.push(newItemWidth);
} else {
const newItemWidth =
percent === 0
? `${minColumnSize}px`
: (containerWidth * percent) / 100 + "px";
gridTemplateColumns.push(newItemWidth);
}
} else {
gridTemplateColumns.push(item);
}
}
if (activeColumnIndex) {
this.addNewColumns(gridTemplateColumns, activeColumnIndex);
}
str = gridTemplateColumns.join(" ");
} else {
const column =
(newContainerWidth * (isSingleTable ? 60 : 100)) / 100 + "px";
const percent = 40 / enableColumns.length;
const otherColumns = (newContainerWidth * percent) / 100 + "px";
str = `${checkboxSize} ${column} `;
for (let col of this.props.columns) {
if (!col.default) {
str += col.enable
? col.defaultSize
? `${col.defaultSize}px `
: `${otherColumns} `
: "0px ";
}
}
str += `${settingsSize}px`;
}
container.style.gridTemplateColumns = str;
if (this.headerRef.current) {
this.headerRef.current.style.gridTemplateColumns = str;
this.headerRef.current.style.width = containerWidth + "px";
}
localStorage.setItem(columnStorageName, str);
};
onChange = (checked) => {
this.props.setSelected(checked);
};
render() {
const {
columns,
sortBy,
sorted,
isHeaderVisible,
checkboxOptions,
containerRef,
onChange,
isChecked,
isIndeterminate,
headerMenu,
columnStorageName,
hasAccess,
...rest
} = this.props;
//console.log("TABLE HEADER RENDER", columns);
return (
<>
{isHeaderVisible ? (
<TableGroupMenu
checkboxOptions={checkboxOptions}
containerRef={containerRef}
onChange={onChange}
isChecked={isChecked}
isIndeterminate={isIndeterminate}
headerMenu={headerMenu}
columnStorageName={columnStorageName}
{...rest}
/>
) : (
<StyledTableHeader
className="table-container_header"
ref={this.headerRef}
{...rest}
>
<StyledTableRow>
{hasAccess ? (
<Checkbox
className="table-container_header-checkbox"
onChange={this.onChange}
isChecked={false}
/>
) : (
<div></div>
)}
{columns.map((column, index) => {
const nextColumn = this.getNextColumn(columns, index);
const resizable = nextColumn ? nextColumn.resizable : false;
return (
<TableHeaderCell
key={column.key}
index={index}
column={column}
sorted={sorted}
sortBy={sortBy}
resizable={resizable}
onMouseDown={this.onMouseDown}
/>
);
})}
<div className="table-container_header-settings">
<TableSettings columns={columns} />
</div>
</StyledTableRow>
</StyledTableHeader>
)}
<StyledEmptyTableContainer />
</>
);
}
}
TableHeader.defaultProps = {
hasAccess: true,
};
TableHeader.propTypes = {
containerRef: PropTypes.shape({ current: PropTypes.any }).isRequired,
columns: PropTypes.array.isRequired,
setSelected: PropTypes.func.isRequired,
sortBy: PropTypes.string,
sorted: PropTypes.bool,
columnStorageName: PropTypes.string,
checkboxSize: PropTypes.string,
sectionWidth: PropTypes.number,
isHeaderVisible: PropTypes.bool,
checkboxOptions: PropTypes.any.isRequired,
isChecked: PropTypes.bool,
onChange: PropTypes.func,
isIndeterminate: PropTypes.bool,
headerMenu: PropTypes.arrayOf(PropTypes.object),
onClick: PropTypes.func,
hasAccess: PropTypes.bool,
};
export default TableHeader;

View File

@ -0,0 +1,85 @@
import React from "react";
import PropTypes from "prop-types";
import Text from "../text";
import Link from "../link";
import IconButton from "../icon-button";
import globalColors from "../utils/globalColors";
import { StyledTableHeaderCell } from "./StyledTableContainer";
const TableHeaderCell = ({
column,
index,
onMouseDown,
resizable,
sortBy,
sorted,
}) => {
const { options, title, enable, active, minWidth } = column;
const isActive = column.sortBy === sortBy || active;
const onClick = (e) => {
column.onClick(column.sortBy, e);
};
return (
<StyledTableHeaderCell
sorted={sorted}
isActive={isActive}
className="table-container_header-cell"
id={`column_${index + 1}`}
data-enable={enable}
data-min-width={minWidth}
>
<div className="table-container_header-item">
{column.onClick ? (
<div className="header-container-text-wrapper">
<Link
onClick={onClick}
fontWeight={600}
color={globalColors.gray}
className="header-container-text"
data={options}
noHover
>
{enable ? title : ""}
</Link>
<IconButton
onClick={column.onIconClick ? column.onIconClick : onClick}
iconName="/static/images/folder arrow.react.svg"
className="header-container-text-icon"
size="small"
/>
</div>
) : (
<Text
fontWeight={600}
color={globalColors.gray}
className="header-container-text"
>
{enable ? title : ""}
</Text>
)}
{resizable && (
<div
data-column={`${index + 1}`}
className="resize-handle not-selectable"
onMouseDown={onMouseDown}
/>
)}
</div>
</StyledTableHeaderCell>
);
};
TableHeaderCell.propTypes = {
column: PropTypes.object,
index: PropTypes.number,
onMouseDown: PropTypes.func,
resizable: PropTypes.bool,
sorted: PropTypes.bool,
sortBy: PropTypes.string,
};
export default TableHeaderCell;

View File

@ -0,0 +1,110 @@
import React, { useRef } from "react";
import PropTypes from "prop-types";
import { StyledTableRow } from "./StyledTableContainer";
import TableCell from "./TableCell";
import ContextMenu from "../context-menu";
import ContextMenuButton from "../context-menu-button";
import Checkbox from "../checkbox";
const TableRow = (props) => {
const {
fileContextClick,
children,
contextOptions,
checked,
element,
onContentSelect,
item,
className,
style,
selectionProp,
hasAccess,
...rest
} = props;
const cm = useRef();
const row = useRef();
const onContextMenu = (e) => {
fileContextClick && fileContextClick();
if (cm.current && !cm.current.menuRef.current) {
row.current.click(e);
}
cm.current.show(e);
};
const renderContext =
Object.prototype.hasOwnProperty.call(props, "contextOptions") &&
contextOptions.length > 0;
const getOptions = () => {
fileContextClick && fileContextClick();
return contextOptions;
};
const onChange = (e) => {
onContentSelect && onContentSelect(e.target.checked, item);
};
return (
<StyledTableRow
onContextMenu={onContextMenu}
className={`${className} table-container_row`}
{...rest}
>
<TableCell
hasAccess={hasAccess}
checked={checked}
{...selectionProp}
style={style}
className={`${selectionProp?.className} table-container_row-checkbox-wrapper`}
>
<div className="table-container_element">{element}</div>
<Checkbox
className="table-container_row-checkbox"
onChange={onChange}
isChecked={checked}
/>
</TableCell>
{children}
<div>
<TableCell {...selectionProp} style={style} forwardedRef={row}>
<ContextMenu ref={cm} model={contextOptions}></ContextMenu>
{renderContext ? (
<ContextMenuButton
color="#A3A9AE"
hoverColor="#657077"
className="expandButton"
getData={getOptions}
directionX="right"
isNew={true}
onClick={onContextMenu}
/>
) : (
<div className="expandButton"> </div>
)}
</TableCell>
</div>
</StyledTableRow>
);
};
TableRow.defaultProps = {
hasAccess: true,
};
TableRow.propTypes = {
fileContextClick: PropTypes.func,
children: PropTypes.any,
contextOptions: PropTypes.array,
checked: PropTypes.bool,
element: PropTypes.any,
onContentSelect: PropTypes.func,
item: PropTypes.object,
selectionProp: PropTypes.object,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
style: PropTypes.object,
hasAccess: PropTypes.bool,
};
export default TableRow;

View File

@ -0,0 +1,70 @@
import React, { useRef, useState } from "react";
import PropTypes from "prop-types";
import IconButton from "../icon-button";
import DropDown from "../drop-down";
import { StyledTableSettings } from "./StyledTableContainer";
import Checkbox from "../checkbox";
const TableSettings = ({ columns }) => {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef();
const onClick = () => {
setIsOpen(!isOpen);
};
const clickOutsideAction = (e) => {
const path = e.path || (e.composedPath && e.composedPath());
const dropDownItem = path ? path.find((x) => x === ref.current) : null;
if (dropDownItem) return;
setIsOpen(false);
};
return (
<StyledTableSettings
className="table-container_header-settings-icon"
ref={ref}
>
<IconButton
color="#A3A9AE"
hoverColor="#657077"
size={12}
isFill
iconName="/static/images/settings.react.svg"
onClick={onClick}
/>
<DropDown
className="table-container_settings"
directionX="right"
open={isOpen}
clickOutsideAction={clickOutsideAction}
withBackdrop={false}
>
{columns.map((column) => {
const onChange = (e) =>
column.onChange && column.onChange(column.key, e);
return (
column.onChange && (
<Checkbox
className="table-container_settings-checkbox"
isChecked={column.enable}
onChange={onChange}
key={column.key}
label={column.title}
/>
)
);
})}
</DropDown>
</StyledTableSettings>
);
};
TableSettings.propTypes = {
columns: PropTypes.array.isRequired,
};
export default TableSettings;

View File

@ -0,0 +1 @@
export default from "./TableContainer";

View File

@ -10,7 +10,6 @@ const Text = ({
fontWeight,
color,
textAlign,
className,
...rest
}) => {
return (
@ -21,7 +20,6 @@ const Text = ({
textAlign={textAlign}
as={!as && tag ? tag : as}
title={title}
className={`${className} not-selectable`}
{...rest}
/>
);

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import PropTypes from "prop-types";
import { ReactSVG } from "react-svg";
@ -11,17 +11,14 @@ const ViewSelector = ({
onChangeView,
...rest
}) => {
const [currentView, setCurrentView] = useState(viewAs);
const onChangeViewHandler = (e) => {
if (isDisabled) return;
const el = e.target.closest(".view-selector-icon");
const view = el.dataset.view;
if (view !== currentView) {
if (view !== viewAs) {
const option = viewSettings.find((setting) => view === setting.value);
option.callback && option.callback();
setCurrentView(view);
onChangeView(view);
}
};
@ -42,7 +39,7 @@ const ViewSelector = ({
return (
<IconWrapper
isDisabled={isDisabled}
isChecked={currentView === value}
isChecked={viewAs === value}
firstItem={indx === 0}
lastItem={indx === lastIndx}
key={value}

View File

@ -0,0 +1,3 @@
<svg width="8" height="4" viewBox="0 0 8 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.646447 0.146447C0.841709 -0.0488155 1.15829 -0.0488155 1.35355 0.146447L4 2.79289L6.64645 0.146447C6.84171 -0.0488155 7.15829 -0.0488155 7.35355 0.146447C7.54882 0.341709 7.54882 0.658291 7.35355 0.853553L4.35355 3.85355C4.15829 4.04882 3.84171 4.04882 3.64645 3.85355L0.646447 0.853553C0.451184 0.658291 0.451184 0.341709 0.646447 0.146447Z" fill="#657077"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@ -212,7 +212,7 @@ export default function withContent(WrappedContent) {
};
getStatusByDate = () => {
const { culture, t, item, sectionWidth } = this.props;
const { culture, t, item, sectionWidth, viewAs } = this.props;
const { created, updated, version, fileExst } = item;
const title =
@ -224,11 +224,20 @@ export default function withContent(WrappedContent) {
const date = fileExst ? updated : created;
const dateLabel = new Date(date).toLocaleString(culture);
const mobile = (sectionWidth && sectionWidth <= 375) || isMobile;
const mobile =
(sectionWidth && sectionWidth <= 375) || isMobile || viewAs === "table";
return mobile ? dateLabel : `${title}: ${dateLabel}`;
};
getTableStatusByDate = (create) => {
const { created, updated, fileExst } = this.props.item;
const date = fileExst ? updated : created;
const dateLabel = new Date(date).toLocaleString(this.props.culture);
return dateLabel;
};
render() {
const { itemTitle } = this.state;
const {
@ -240,6 +249,7 @@ export default function withContent(WrappedContent) {
isTrashFolder,
onFilesClick,
viewAs,
element,
} = this.props;
const { id, fileExst, updated, createdBy, access, fileStatus } = item;
@ -247,7 +257,11 @@ export default function withContent(WrappedContent) {
const isEdit = id === fileActionId && fileExst === fileActionExt;
const updatedDate = updated && this.getStatusByDate();
const updatedDate =
viewAs === "table"
? this.getTableStatusByDate(false)
: updated && this.getStatusByDate();
const createdDate = this.getTableStatusByDate(true);
const fileOwner =
createdBy &&
@ -268,6 +282,7 @@ export default function withContent(WrappedContent) {
return isEdit ? (
<EditingWrapperComponent
className={"editing-wrapper-component"}
elementIcon={element}
itemTitle={itemTitle}
itemId={id}
viewAs={viewAs}
@ -279,6 +294,7 @@ export default function withContent(WrappedContent) {
<WrappedContent
titleWithoutExt={titleWithoutExt}
updatedDate={updatedDate}
createdDate={createdDate}
fileOwner={fileOwner}
accessToEdit={accessToEdit}
linkStyles={linkStyles}

View File

@ -10,11 +10,8 @@ export default function withFileActions(WrappedFileItem) {
class WithFileActions extends React.Component {
constructor(props) {
super(props);
this.state = {
isMouseDown: false,
};
}
onContentFileSelect = (checked, file) => {
const { selectRowAction } = this.props;
if (!file || file.id === -1) return;
@ -54,8 +51,6 @@ export default function withFileActions(WrappedFileItem) {
} = this.props;
const notSelectable = e.target.classList.contains("not-selectable");
this.setState({ isMouseDown: true });
if (!draggable || isPrivacy) return;
if (window.innerWidth < 1025 || notSelectable) {
@ -78,9 +73,8 @@ export default function withFileActions(WrappedFileItem) {
onMarkAsRead = (id) =>
this.props.markAsRead([], [`${id}`], this.props.item);
onMouseUpHandler = (e) => {
const { isMouseDown } = this.state;
const { viewAs } = this.props;
onMouseClick = (e) => {
const { viewAs, isItemsSelected } = this.props;
if (
e.target.closest(".checkbox") ||
@ -89,23 +83,19 @@ export default function withFileActions(WrappedFileItem) {
e.target.tagName === "A" ||
e.target.closest(".expandButton") ||
e.target.closest(".badges") ||
e.button !== 0
e.button !== 0 /* ||
isItemsSelected */
)
return;
if (viewAs === "tile") {
if (
!isMouseDown ||
e.target.closest(".edit-button") ||
e.target.tagName === "IMG"
)
if (e.target.closest(".edit-button") || e.target.tagName === "IMG")
return;
this.onFilesClick();
} else {
this.fileContextClick();
}
this.setState({ isMouseDown: false });
};
onFilesClick = (e) => {
const {
@ -220,7 +210,7 @@ export default function withFileActions(WrappedFileItem) {
const isDragging = isFolder && access < 2 && !isTrashFolder && !isPrivacy;
let className = isDragging ? " droppable" : "";
if (draggable) className += " draggable not-selectable";
if (draggable) className += " draggable";
let value = fileExst || contentLength ? `file_${id}` : `folder_${id}`;
value += draggable ? "_draggable" : "";
@ -251,7 +241,7 @@ export default function withFileActions(WrappedFileItem) {
onDrop={this.onDrop}
onMouseDown={this.onMouseDown}
onFilesClick={this.onFilesClick}
onMouseUp={this.onMouseUpHandler}
onMouseClick={this.onMouseClick}
getClassName={this.getClassName}
className={className}
isDragging={isDragging}
@ -376,6 +366,7 @@ export default function withFileActions(WrappedFileItem) {
setConvertDialogVisible,
isDesktop: auth.settingsStore.isDesktopClient,
personal: auth.settingsStore.personal,
isItemsSelected: selection.length > 0,
};
}
)(observer(WithFileActions));

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import styled from "styled-components";
import styled, { css } from "styled-components";
import Button from "@appserver/components/button";
import TextInput from "@appserver/components/text-input";
import commonIconsStyles from "@appserver/components/utils/common-icons-style";
@ -37,6 +37,17 @@ const EditingWrapper = styled.div`
display: inline-flex;
align-items: center;
${(props) =>
props.viewAs === "table" &&
css`
grid-column-start: 1;
grid-column-end: -1;
border-bottom: 1px solid #eceef1;
padding-bottom: 4px;
margin-top: 4px;
`}
${(props) =>
props.viewAs === "tile" &&
`margin-right: 12px !important; margin-left: -4px;`}
@ -60,6 +71,19 @@ const EditingWrapper = styled.div`
height: 32px;
padding: 8px 7px 7px 7px;
${(props) =>
props.viewAs === "table" &&
css`
width: 24px;
height: 24px;
border: 1px transparent;
padding: 4px 0 0 0;
:hover {
border: 1px solid #d0d5da;
}
`}
&:last-child {
margin-left: 4px;
}
@ -76,6 +100,10 @@ const EditingWrapper = styled.div`
width: 14px;
height: 14px;
}
.is-edit {
margin-top: 4px;
}
`;
const EditingWrapperComponent = (props) => {
@ -87,8 +115,11 @@ const EditingWrapperComponent = (props) => {
cancelUpdateItem,
isLoading,
viewAs,
elementIcon,
} = props;
const isTable = viewAs === "table";
const [OkIconIsHovered, setIsHoveredOk] = useState(false);
const [CancelIconIsHovered, setIsHoveredCancel] = useState(false);
@ -116,6 +147,7 @@ const EditingWrapperComponent = (props) => {
return (
<EditingWrapper viewAs={viewAs}>
{isTable && elementIcon}
<TextInput
className="edit-text"
name="title"
@ -129,6 +161,7 @@ const EditingWrapperComponent = (props) => {
onFocus={onFocus}
isDisabled={isLoading}
data-itemid={itemId}
withBorder={!isTable}
/>
<Button
className="edit-button not-selectable"

View File

@ -135,7 +135,7 @@ export const loopTreeFolders = (
}
loopTreeFolders(
newPath,
newItems.folders,
newItems.folders ? newItems.folders : [],
folders,
foldersCount,
currentFolder

View File

@ -1,35 +1,42 @@
import React from "react";
import React, { useEffect } from "react";
import { inject, observer } from "mobx-react";
import RowContainer from "@appserver/components/row-container";
import { Consumer } from "@appserver/components/utils/context";
import SimpleFilesRow from "./SimpleFilesRow";
const FilesRowContainer = ({ filesList }) => {
const FilesRowContainer = ({ filesList, sectionWidth, viewAs, setViewAs }) => {
useEffect(() => {
if (viewAs !== "table" && viewAs !== "row") return;
if (sectionWidth < 1025) {
viewAs !== "row" && setViewAs("row");
} else {
viewAs !== "table" && setViewAs("table");
}
}, [sectionWidth]);
return (
<Consumer>
{(context) => (
<RowContainer
className="files-row-container"
draggable
useReactWindow={false}
>
{filesList.map((item, index) => (
<SimpleFilesRow
key={`${item.id}_${index}`}
item={item}
sectionWidth={context.sectionWidth}
/>
))}
</RowContainer>
)}
</Consumer>
<RowContainer
className="files-row-container"
draggable
useReactWindow={false}
>
{filesList.map((item, index) => (
<SimpleFilesRow
key={`${item.id}_${index}`}
item={item}
sectionWidth={sectionWidth}
/>
))}
</RowContainer>
);
};
export default inject(({ filesStore }) => {
const { filesList } = filesStore;
const { filesList, viewAs, setViewAs } = filesStore;
return {
filesList,
viewAs,
setViewAs,
};
})(observer(FilesRowContainer));

View File

@ -67,7 +67,7 @@ const SimpleFilesRow = (props) => {
contextOptionsProps,
checkedProps,
onFilesClick,
onMouseUp,
onMouseClick,
isEdit,
showShare,
} = props;
@ -95,7 +95,6 @@ const SimpleFilesRow = (props) => {
onDrop={onDrop}
onMouseDown={onMouseDown}
dragging={dragging && isDragging}
{...contextOptionsProps}
>
<StyledSimpleFilesRow
key={item.id}
@ -107,7 +106,7 @@ const SimpleFilesRow = (props) => {
onSelect={onContentFileSelect}
rowContextClick={fileContextClick}
isPrivacy={isPrivacy}
onMouseUp={onMouseUp}
onClick={onMouseClick}
onDoubleClick={onFilesClick}
checked={checkedProps}
{...contextOptionsProps}

View File

@ -0,0 +1,41 @@
import React, { useEffect, useRef } from "react";
import TableContainer from "@appserver/components/table-container";
import { inject, observer } from "mobx-react";
import TableRow from "./TableRow";
import TableHeader from "./TableHeader";
import TableBody from "@appserver/components/table-container/TableBody";
const Table = ({ filesList, sectionWidth, viewAs, setViewAs }) => {
const ref = useRef(null);
useEffect(() => {
if (viewAs !== "table" && viewAs !== "row") return;
if (sectionWidth < 1025) {
viewAs !== "row" && setViewAs("row");
} else {
viewAs !== "table" && setViewAs("table");
}
}, [sectionWidth]);
return (
<TableContainer forwardedRef={ref}>
<TableHeader sectionWidth={sectionWidth} containerRef={ref} />
<TableBody>
{filesList.map((item) => (
<TableRow key={item.id} item={item} />
))}
</TableBody>
</TableContainer>
);
};
export default inject(({ filesStore }) => {
const { filesList, viewAs, setViewAs } = filesStore;
return {
filesList,
viewAs,
setViewAs,
};
})(observer(Table));

View File

@ -0,0 +1,280 @@
import React from "react";
import TableHeader from "@appserver/components/table-container/TableHeader";
import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import { FilterType } from "@appserver/common/constants";
import DropDownItem from "@appserver/components/drop-down-item";
const TABLE_COLUMNS = "filesTableColumns";
class FilesTableHeader extends React.Component {
constructor(props) {
super(props);
const { t, withContent } = props;
const defaultColumns = [
{
key: "Name",
title: t("Common:Name"),
resizable: true,
enable: true,
default: true,
sortBy: "AZ",
minWidth: 180,
onClick: this.onFilter,
},
{
key: "Author",
title: t("ByAuthor"),
enable: true,
resizable: true,
sortBy: "Author",
onClick: this.onFilter,
onChange: this.onColumnChange,
},
{
key: "Created",
title: t("ByCreationDate"),
enable: false,
resizable: true,
sortBy: "DateAndTimeCreation",
onClick: this.onFilter,
onChange: this.onColumnChange,
},
{
key: "Modified",
title: t("ByLastModifiedDate"),
enable: true,
resizable: true,
sortBy: "DateAndTime",
onClick: this.onFilter,
onChange: this.onColumnChange,
},
{
key: "Size",
title: t("Common:Size"),
enable: true,
resizable: true,
sortBy: "Size",
onClick: this.onFilter,
onChange: this.onColumnChange,
},
{
key: "Type",
title: t("Common:Type"),
enable: false,
resizable: true,
sortBy: "Type",
onClick: this.onFilter,
onChange: this.onColumnChange,
},
{
key: "Share",
title: "",
enable: withContent,
defaultSize: 80,
resizable: false,
},
];
const columns = this.getColumns(defaultColumns);
this.state = { columns };
}
componentDidUpdate(prevProps) {
const { columns } = this.state;
if (this.props.withContent !== prevProps.withContent) {
const columnIndex = columns.findIndex((c) => c.key === "Share");
if (columnIndex === -1) return;
columns[columnIndex].enable = this.props.withContent;
this.setState({ columns });
}
}
getColumns = (defaultColumns) => {
const storageColumns = localStorage.getItem(TABLE_COLUMNS);
const columns = [];
if (storageColumns) {
const splitColumns = storageColumns.split(",");
for (let col of defaultColumns) {
const column = splitColumns.find((key) => key === col.key);
column ? (col.enable = true) : (col.enable = false);
columns.push(col);
}
return columns;
} else {
return defaultColumns;
}
};
onColumnChange = (key, e) => {
const { columns } = this.state;
const columnIndex = columns.findIndex((c) => c.key === key);
if (columnIndex === -1) return;
columns[columnIndex].enable = !columns[columnIndex].enable;
this.setState({ columns });
const tableColumns = columns.map((c) => c.enable && c.key);
localStorage.setItem(TABLE_COLUMNS, tableColumns);
};
onFilter = (sortBy) => {
const { filter, selectedFolderId, setIsLoading, fetchFiles } = this.props;
const newFilter = filter.clone();
if (newFilter.sortBy !== sortBy) {
newFilter.sortBy = sortBy;
} else {
newFilter.sortOrder =
newFilter.sortOrder === "ascending" ? "descending" : "ascending";
}
setIsLoading(true);
fetchFiles(selectedFolderId, newFilter).finally(() => setIsLoading(false));
};
onChange = (checked) => {
this.props.setSelected(checked ? "all" : "none");
};
onSelect = (e) => {
const key = e.currentTarget.dataset.key;
this.props.setSelected(key);
};
setSelected = (checked) => {
this.props.setSelected && this.props.setSelected(checked ? "all" : "none");
};
render() {
const {
t,
containerRef,
isHeaderVisible,
isHeaderChecked,
isHeaderIndeterminate,
getHeaderMenu,
filter,
sectionWidth,
} = this.props;
const { sortBy, sortOrder } = filter;
const { columns } = this.state;
const checkboxOptions = (
<>
<DropDownItem label={t("All")} data-key="all" onClick={this.onSelect} />
<DropDownItem
label={t("Translations:Folders")}
data-key={FilterType.FoldersOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("Common:Documents")}
data-key={FilterType.DocumentsOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("Translations:Presentations")}
data-key={FilterType.PresentationsOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("Translations:Spreadsheets")}
data-key={FilterType.SpreadsheetsOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("Images")}
data-key={FilterType.ImagesOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("Media")}
data-key={FilterType.MediaOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("Archives")}
data-key={FilterType.ArchiveOnly}
onClick={this.onSelect}
/>
<DropDownItem
label={t("AllFiles")}
data-key={FilterType.FilesOnly}
onClick={this.onSelect}
/>
</>
);
return (
<TableHeader
checkboxSize="32px"
sorted={sortOrder === "descending"}
sortBy={sortBy}
setSelected={this.setSelected}
containerRef={containerRef}
columns={columns}
columnStorageName="filesColumnsSize"
sectionWidth={sectionWidth}
isHeaderVisible={isHeaderVisible}
checkboxOptions={checkboxOptions}
onChange={this.onChange}
isChecked={isHeaderChecked}
isIndeterminate={isHeaderIndeterminate}
headerMenu={getHeaderMenu(t)}
/>
);
}
}
export default inject(
({
filesStore,
filesActionsStore,
selectedFolderStore,
treeFoldersStore,
}) => {
const {
setSelected,
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
setIsLoading,
filter,
fetchFiles,
canShare,
} = filesStore;
const { getHeaderMenu } = filesActionsStore;
const { isPrivacyFolder } = treeFoldersStore;
const withContent = canShare || (canShare && isPrivacyFolder && isDesktop);
return {
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
filter,
selectedFolderId: selectedFolderStore.id,
withContent,
setSelected,
setIsLoading,
fetchFiles,
getHeaderMenu,
};
}
)(
withTranslation(["Home", "Common", "Translations"])(
observer(FilesTableHeader)
)
);

View File

@ -0,0 +1,190 @@
import React, { useState } from "react";
import { withRouter } from "react-router";
import withContent from "../../../../../HOCs/withContent";
import withBadges from "../../../../../HOCs/withBadges";
import withFileActions from "../../../../../HOCs/withFileActions";
import withContextOptions from "../../../../../HOCs/withContextOptions";
import ItemIcon from "../../../../../components/ItemIcon";
import SharedButton from "../../../../../components/SharedButton";
import { withTranslation } from "react-i18next";
import TableRow from "@appserver/components/table-container/TableRow";
import TableCell from "@appserver/components/table-container/TableCell";
import DragAndDrop from "@appserver/components/drag-and-drop";
import FileNameCell from "./sub-components/FileNameCell";
import SizeCell from "./sub-components/SizeCell";
import AuthorCell from "./sub-components/AuthorCell";
import DateCell from "./sub-components/DateCell";
import TypeCell from "./sub-components/TypeCell";
import globalColors from "@appserver/components/utils/globalColors";
import styled from "styled-components";
import Base from "@appserver/components/themes/base";
const sideColor = globalColors.gray;
const { acceptBackground, background } = Base.dragAndDrop;
const StyledDragAndDrop = styled(DragAndDrop)`
display: contents;
`;
const StyledShare = styled.div`
cursor: pointer;
.share-button {
padding: 4px;
border: 1px solid transparent;
border-radius: 3px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
:hover {
border: 1px solid #a3a9ae;
svg {
cursor: pointer;
}
}
.share-button-icon {
margin-right: 7px;
}
}
`;
const StyledBadgesContainer = styled.div`
display: flex;
align-items: center;
height: 19px;
margin-left: 8px;
.badges {
display: flex;
align-items: center;
height: 19px;
margin-right: 12px;
}
.badge {
cursor: pointer;
height: 14px;
width: 14px;
margin-right: 6px;
}
`;
const FilesTableRow = (props) => {
const {
t,
contextOptionsProps,
fileContextClick,
item,
onContentFileSelect,
checkedProps,
className,
value,
onMouseClick,
badgesComponent,
dragging,
isDragging,
onDrop,
onMouseDown,
showShare,
} = props;
const sharedButton =
item.canShare && showShare ? (
<SharedButton
t={t}
id={item.id}
shared={item.shared}
isFolder={item.isFolder}
/>
) : null;
const element = (
<ItemIcon id={item.id} icon={item.icon} fileExst={item.fileExst} />
);
const selectionProp = {
className: `files-item ${className} ${value}`,
value,
};
const [isDragActive, setIsDragActive] = useState(false);
const dragStyles = {
style: {
background:
dragging && isDragging
? isDragActive
? acceptBackground
: background
: "none",
},
};
const onDragOver = (dragActive) => {
if (dragActive !== isDragActive) {
setIsDragActive(dragActive);
}
};
const onDragLeave = () => {
setIsDragActive(false);
};
return (
<StyledDragAndDrop
data-title={item.title}
value={value}
className={`files-item ${className}`}
onDrop={onDrop}
onMouseDown={onMouseDown}
dragging={dragging && isDragging}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
>
<TableRow
{...dragStyles}
dragging={dragging && isDragging}
selectionProp={selectionProp}
key={item.id}
item={item}
element={element}
fileContextClick={fileContextClick}
onContentSelect={onContentFileSelect}
onClick={onMouseClick}
{...contextOptionsProps}
checked={checkedProps}
>
<TableCell {...dragStyles} {...selectionProp}>
<FileNameCell {...props} />
<StyledBadgesContainer>{badgesComponent}</StyledBadgesContainer>
</TableCell>
<TableCell {...dragStyles} {...selectionProp}>
<AuthorCell sideColor={sideColor} {...props} />
</TableCell>
<TableCell {...dragStyles} {...selectionProp}>
<DateCell create sideColor={sideColor} {...props} />
</TableCell>
<TableCell {...dragStyles} {...selectionProp}>
<DateCell sideColor={sideColor} {...props} />
</TableCell>
<TableCell {...dragStyles} {...selectionProp}>
<SizeCell sideColor={sideColor} {...props} />
</TableCell>
<TableCell {...dragStyles} {...selectionProp}>
<TypeCell sideColor={sideColor} {...props} />
</TableCell>
<TableCell {...dragStyles} {...selectionProp}>
<StyledShare>{sharedButton}</StyledShare>
</TableCell>
</TableRow>
</StyledDragAndDrop>
);
};
export default withTranslation("Home")(
withFileActions(
withRouter(withContextOptions(withContent(withBadges(FilesTableRow))))
)
);

View File

@ -0,0 +1,24 @@
import React from "react";
import { StyledText, StyledAuthorAvatar } from "./CellStyles";
const AuthorCell = ({ fileOwner, sideColor, item }) => {
return (
<>
<StyledAuthorAvatar
src={item.createdBy.avatarSmall}
className="author-avatar-cell"
/>
<StyledText
color={sideColor}
fontSize="12px"
fontWeight={400}
title={fileOwner}
truncate
>
{fileOwner}
</StyledText>
</>
);
};
export default AuthorCell;

View File

@ -0,0 +1,16 @@
import styled from "styled-components";
import Text from "@appserver/components/text";
const StyledText = styled(Text)`
display: inline-block;
margin-right: 12px;
`;
const StyledAuthorAvatar = styled.img`
width: 16px;
height: 16px;
margin-right: 8px;
border-radius: 20px;
`;
export { StyledText, StyledAuthorAvatar };

View File

@ -0,0 +1,23 @@
import React from "react";
import { StyledText } from "./CellStyles";
const DateCell = ({ create, updatedDate, createdDate, sideColor, item }) => {
const { fileExst, contentLength, providerKey } = item;
const date = create ? createdDate : updatedDate;
return (
<StyledText
title={date}
fontSize="12px"
fontWeight={400}
color={sideColor}
className="row_update-text"
truncate
>
{(fileExst || contentLength || !providerKey) && date && date}
</StyledText>
);
};
export default DateCell;

View File

@ -0,0 +1,35 @@
import React from "react";
import Link from "@appserver/components/link";
import Text from "@appserver/components/text";
const FileNameCell = ({ item, titleWithoutExt, linkStyles }) => {
const { fileExst } = item;
return (
<Link
type="page"
title={titleWithoutExt}
fontWeight="600"
fontSize="15px"
{...linkStyles}
color="#333"
isTextOverflow
>
{titleWithoutExt}
{fileExst ? (
<Text
className="badge-ext"
as="span"
color="#A3A9AE"
fontSize="15px"
fontWeight={600}
title={fileExst}
truncate={true}
>
{fileExst}
</Text>
) : null}
</Link>
);
};
export default FileNameCell;

View File

@ -0,0 +1,31 @@
import React from "react";
import { StyledText } from "./CellStyles";
const SizeCell = ({ t, item, sideColor }) => {
const {
fileExst,
contentLength,
providerKey,
filesCount,
foldersCount,
} = item;
return (
<StyledText
color={sideColor}
fontSize="12px"
fontWeight={400}
title=""
truncate
>
{fileExst || contentLength
? contentLength
: !providerKey
? `${t("TitleDocuments")}: ${filesCount} | ${t(
"TitleSubfolders"
)}: ${foldersCount}`
: ""}
</StyledText>
);
};
export default SizeCell;

View File

@ -0,0 +1,40 @@
import React from "react";
import { StyledText } from "./CellStyles";
import { FileType } from "@appserver/common/constants";
const TypeCell = ({ t, item, sideColor }) => {
const { fileExst, fileType } = item;
const getItemType = () => {
switch (fileType) {
case FileType.Unknown:
return t("Common:Unknown");
case FileType.Archive:
return t("Common:Archive");
case FileType.Video:
return t("Common:Video");
case FileType.Audio:
return t("Common:Audio");
case FileType.Image:
return t("Common:Image");
case FileType.Spreadsheet:
return t("Spreadsheet");
case FileType.Presentation:
return t("Presentation");
case FileType.Document:
return t("Document");
default:
return t("Folder");
}
};
const type = getItemType();
const Exst = fileExst ? fileExst.slice(1).toUpperCase() : "";
return (
<StyledText fontSize="12px" fontWeight="400" color={sideColor} truncate>
{type} {Exst}
</StyledText>
);
};
export default TypeCell;

View File

@ -33,7 +33,7 @@ const FilesTile = (props) => {
//element,
getIcon,
onFilesClick,
onMouseUp,
onMouseClick,
showShare,
} = props;
const temporaryIcon = getIcon(
@ -80,7 +80,7 @@ const FilesTile = (props) => {
tileContextClick={fileContextClick}
isPrivacy={isPrivacy}
dragging={dragging && isDragging}
onMouseUp={onMouseUp}
onClick={onMouseClick}
thumbnailClick={onFilesClick}
onDoubleClick={onFilesClick}
{...checkedProps}

View File

@ -1,32 +1,25 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { Consumer } from "@appserver/components/utils/context";
import TileContainer from "./sub-components/TileContainer";
import FileTile from "./FileTile";
const FilesTileContainer = ({ filesList, t }) => {
const FilesTileContainer = ({ filesList, t, sectionWidth }) => {
return (
<Consumer>
{(context) => (
<TileContainer
className="tile-container"
draggable
useReactWindow={false}
headingFolders={t("Folders")}
headingFiles={t("Files")}
>
{filesList.map((item, index) => (
<FileTile
key={`${item.id}_${index}`}
item={item}
sectionWidth={context.sectionWidth}
/>
))}
</TileContainer>
)}
</Consumer>
<TileContainer
className="tile-container"
draggable
useReactWindow={false}
headingFolders={t("Folders")}
headingFiles={t("Files")}
>
{filesList.map((item, index) => (
<FileTile
key={`${item.id}_${index}`}
item={item}
sectionWidth={sectionWidth}
/>
))}
</TileContainer>
);
};

View File

@ -7,6 +7,8 @@ import FilesRowContainer from "./RowsView/FilesRowContainer";
import FilesTileContainer from "./TilesView/FilesTileContainer";
import EmptyContainer from "../../../../components/EmptyContainer";
import withLoader from "../../../../HOCs/withLoader";
import TableView from "./TableView/TableContainer";
import { Consumer } from "@appserver/components/utils/context";
let currentDroppable = null;
@ -26,6 +28,7 @@ const SectionBodyContent = (props) => {
moveDragItems,
viewAs,
setSelection,
setViewAs,
} = props;
useEffect(() => {
@ -44,6 +47,7 @@ const SectionBodyContent = (props) => {
document.addEventListener("dragover", onDragOver);
document.addEventListener("dragleave", onDragLeaveDoc);
document.addEventListener("drop", onDropEvent);
return () => {
window.removeEventListener("mousedown", onMouseDown);
window.removeEventListener("mouseup", onMouseUp);
@ -79,13 +83,31 @@ const SectionBodyContent = (props) => {
const droppable = wrapperElement.closest(".droppable");
if (currentDroppable !== droppable) {
if (currentDroppable) {
currentDroppable.classList.remove("droppable-hover");
if (viewAs === "table") {
const value = currentDroppable.getAttribute("value");
const classElements = document.getElementsByClassName(value);
for (let cl of classElements) {
cl.classList.remove("droppable-hover");
}
} else {
currentDroppable.classList.remove("droppable-hover");
}
}
currentDroppable = droppable;
if (currentDroppable) {
currentDroppable.classList.add("droppable-hover");
currentDroppable = droppable;
if (viewAs === "table") {
const value = currentDroppable.getAttribute("value");
const classElements = document.getElementsByClassName(value);
for (let cl of classElements) {
cl.classList.add("droppable-hover");
}
} else {
currentDroppable.classList.add("droppable-hover");
currentDroppable = droppable;
}
}
}
};
@ -154,12 +176,23 @@ const SectionBodyContent = (props) => {
};
//console.log("Files Home SectionBodyContent render", props);
return (!fileActionId && isEmptyFilesList) || null ? (
<EmptyContainer />
) : viewAs === "tile" ? (
<FilesTileContainer t={t} />
) : (
<FilesRowContainer tReady={tReady} />
return (
<Consumer>
{(context) =>
(!fileActionId && isEmptyFilesList) || null ? (
<EmptyContainer />
) : viewAs === "tile" ? (
<FilesTileContainer sectionWidth={context.sectionWidth} t={t} />
) : viewAs === "table" ? (
<TableView sectionWidth={context.sectionWidth} tReady={tReady} />
) : (
<FilesRowContainer
sectionWidth={context.sectionWidth}
tReady={tReady}
/>
)
}
</Consumer>
);
};
@ -180,6 +213,7 @@ export default inject(
startDrag,
setStartDrag,
setSelection,
setViewAs,
} = filesStore;
return {
@ -197,6 +231,7 @@ export default inject(
moveDragItems: filesActionsStore.moveDragItems,
viewAs,
setSelection,
setViewAs,
};
}
)(

View File

@ -94,7 +94,14 @@ class SectionFilterContent extends React.Component {
onChangeViewAs = (view) => {
const { setViewAs } = this.props;
setViewAs(view);
//const tabletView = isTabletView();
if (view === "row") {
//tabletView ? setViewAs("table") : setViewAs("row");
setViewAs("table");
} else {
setViewAs(view);
}
};
getData = () => {
@ -270,15 +277,11 @@ class SectionFilterContent extends React.Component {
{
value: "row",
label: t("ViewList"),
isSetting: isMobileOnly,
default: true,
icon: "/static/images/view-rows.react.svg",
},
{
value: "tile",
label: t("ViewTiles"),
isSetting: isMobileOnly,
default: true,
icon: "/static/images/view-tiles.react.svg",
callback: createThumbnails,
},

View File

@ -216,7 +216,6 @@ class SectionHeaderContent extends React.Component {
.downloadAction(this.props.t("Translations:ArchivingData"))
.catch((err) => toastr.error(err));
downloadAsAction = () => this.props.setDownloadDialogVisible(true);
renameAction = () => console.log("renameAction click");
onOpenSharingPanel = () => this.props.setSharingPanelVisible(true);
@ -241,8 +240,6 @@ class SectionHeaderContent extends React.Component {
}
};
onEmptyTrashAction = () => this.props.setEmptyTrashDialogVisible(true);
getContextOptionsFolder = () => {
const { t } = this.props;
return [
@ -311,21 +308,9 @@ class SectionHeaderContent extends React.Component {
};
getMenuItems = () => {
const {
t,
hasSelection,
isAccessedSelected,
isWebEditSelected,
isViewedSelected,
deleteDialogVisible,
isRecycleBin,
isThirdPartySelection,
isPrivacy,
isFavoritesFolder,
isRecentFolder,
isShareFolder,
personal,
} = this.props;
const { t, getHeaderMenu } = this.props;
const headerMenu = getHeaderMenu(t);
let menu = [
{
@ -379,75 +364,9 @@ class SectionHeaderContent extends React.Component {
],
onSelect: this.onSelect,
},
{
label: t("Share"),
disabled: isFavoritesFolder || isRecentFolder || !isAccessedSelected,
onClick: this.onOpenSharingPanel,
},
{
label: t("Common:Download"),
disabled: !hasSelection,
onClick: this.downloadAction,
},
{
label: t("Translations:DownloadAs"),
disabled: !hasSelection || !isWebEditSelected,
onClick: this.downloadAsAction,
},
{
label: t("MoveTo"),
disabled:
isFavoritesFolder ||
isRecentFolder ||
!isAccessedSelected ||
!hasSelection ||
isThirdPartySelection,
onClick: this.onMoveAction,
},
{
label: t("Translations:Copy"),
disabled: !hasSelection,
onClick: this.onCopyAction,
},
{
label: t("Common:Delete"),
disabled:
!hasSelection || !deleteDialogVisible || isThirdPartySelection,
onClick: this.onDeleteAction,
},
];
if (isRecycleBin) {
menu.push({
label: t("EmptyRecycleBin"),
onClick: this.onEmptyTrashAction,
});
menu.splice(4, 2, {
label: t("Translations:Restore"),
onClick: this.onMoveAction,
});
menu.splice(1, 1);
}
if (isPrivacy) {
menu.splice(1, 1);
menu.splice(2, 1);
menu.splice(3, 1);
}
if (isShareFolder) {
menu.splice(4, 1);
}
if (isRecentFolder || isFavoritesFolder) {
menu.splice(1, 1);
}
if (personal && !isWebEditSelected && !isViewedSelected) {
menu.splice(1, 1);
}
menu = [...menu, ...headerMenu];
return menu;
};
@ -467,6 +386,7 @@ class SectionHeaderContent extends React.Component {
isDesktop,
isTabletView,
personal,
viewAs,
} = this.props;
const menuItems = this.getMenuItems();
@ -482,7 +402,7 @@ class SectionHeaderContent extends React.Component {
isDesktop={isDesktop}
isTabletView={isTabletView}
>
{isHeaderVisible ? (
{isHeaderVisible && viewAs !== "table" ? (
<div className="group-button-menu-container">
<GroupButtonsMenu
checked={isHeaderChecked}
@ -579,7 +499,6 @@ export default inject(
auth,
filesStore,
dialogsStore,
treeFoldersStore,
selectedFolderStore,
filesActionsStore,
settingsStore,
@ -588,38 +507,23 @@ export default inject(
setSelected,
fileActionStore,
fetchFiles,
selection,
filter,
canCreate,
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
userAccess,
isAccessedSelected,
isThirdPartySelection,
isWebEditSelected,
setIsLoading,
isViewedSelected,
hasSelection,
viewAs,
} = filesStore;
const {
isRecycleBinFolder,
isPrivacyFolder,
isFavoritesFolder,
isRecentFolder,
isShareFolder,
} = treeFoldersStore;
const { setAction } = fileActionStore;
const {
setSharingPanelVisible,
setMoveToPanelVisible,
setCopyPanelVisible,
setEmptyTrashDialogVisible,
setDownloadDialogVisible,
setDeleteDialogVisible,
} = dialogsStore;
const { deleteAction, downloadAction } = filesActionsStore;
const { deleteAction, downloadAction, getHeaderMenu } = filesActionsStore;
return {
isDesktop: auth.settingsStore.isDesktopClient,
@ -627,25 +531,15 @@ export default inject(
title: selectedFolderStore.title,
parentId: selectedFolderStore.parentId,
currentFolderId: selectedFolderStore.id,
isRecycleBin: isRecycleBinFolder,
isPrivacy: isPrivacyFolder,
isFavoritesFolder,
isRecentFolder,
isShareFolder,
filter,
canCreate,
hasSelection,
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
deleteDialogVisible: userAccess,
isAccessedSelected,
isThirdPartySelection,
isWebEditSelected,
isViewedSelected,
isTabletView: auth.settingsStore.isTabletView,
confirmDelete: settingsStore.confirmDelete,
personal: auth.settingsStore.personal,
viewAs,
setSelected,
setAction,
@ -654,11 +548,10 @@ export default inject(
setSharingPanelVisible,
setMoveToPanelVisible,
setCopyPanelVisible,
setEmptyTrashDialogVisible,
deleteAction,
setDeleteDialogVisible,
setDownloadDialogVisible,
downloadAction,
getHeaderMenu,
};
}
)(

View File

@ -245,7 +245,6 @@ class PureHome extends React.Component {
uploadFiles
onDrop={isRecycleBinFolder || isPrivacyFolder ? null : this.onDrop}
setSelections={this.props.setSelections}
onMouseMove={this.onMouseMove}
showPrimaryProgressBar={primaryProgressDataVisible}
primaryProgressBarValue={primaryProgressDataPercent}
primaryProgressBarIcon={primaryProgressDataIcon}

View File

@ -645,6 +645,127 @@ class FilesActionStore {
this.uploadDataStore.itemOperationToFolder(operationData);
}
};
getHeaderMenu = (t) => {
const {
isFavoritesFolder,
isRecentFolder,
isRecycleBinFolder,
isPrivacyFolder,
isShareFolder,
} = this.treeFoldersStore;
const {
selection,
isAccessedSelected,
isWebEditSelected,
isThirdPartySelection,
userAccess,
isViewedSelected,
hasSelection,
} = this.filesStore;
const {
setSharingPanelVisible,
setDownloadDialogVisible,
setMoveToPanelVisible,
setCopyPanelVisible,
setDeleteDialogVisible,
setEmptyTrashDialogVisible,
} = this.dialogsStore;
const selectionCount = selection.length;
const headerMenu = [
{
label: t("Share"),
disabled: isFavoritesFolder || isRecentFolder || !isAccessedSelected,
onClick: () => setSharingPanelVisible(true),
},
{
label: t("Common:Download"),
disabled: !hasSelection,
onClick: () =>
this.downloadAction(t("Translations:ArchivingData")).catch((err) =>
toastr.error(err)
),
},
{
label: t("Translations:DownloadAs"),
disabled: !hasSelection || !isWebEditSelected,
onClick: () => setDownloadDialogVisible(true),
},
{
label: t("MoveTo"),
disabled:
isFavoritesFolder ||
isRecentFolder ||
!isAccessedSelected ||
!hasSelection ||
isThirdPartySelection,
onClick: () => setMoveToPanelVisible(true),
},
{
label: t("Translations:Copy"),
disabled: !hasSelection,
onClick: () => setCopyPanelVisible(true),
},
{
label: t("Common:Delete"),
disabled: !hasSelection || isThirdPartySelection,
onClick: () => {
if (this.settingsStore.confirmDelete) {
setDeleteDialogVisible(true);
} else {
const translations = {
deleteOperation: t("Translations:DeleteOperation"),
deleteFromTrash: t("Translations:DeleteFromTrash"),
deleteSelectedElem: t("Translations:DeleteSelectedElem"),
};
this.deleteAction(translations).catch((err) => toastr.error(err));
}
},
},
];
if (isRecycleBinFolder) {
headerMenu.push({
label: t("EmptyRecycleBin"),
onClick: () => setEmptyTrashDialogVisible(true),
});
headerMenu.splice(4, 2, {
label: t("Translations:Restore"),
onClick: () => setMoveToPanelVisible(true),
});
headerMenu.splice(1, 1);
}
if (isPrivacyFolder) {
headerMenu.splice(1, 1);
headerMenu.splice(2, 1);
headerMenu.splice(3, 1);
}
if (isShareFolder) {
headerMenu.splice(4, 1);
}
if (isRecentFolder || isFavoritesFolder) {
headerMenu.splice(1, 1);
}
if (
this.authStore.settingsStore.personal &&
!isWebEditSelected &&
!isViewedSelected
) {
headerMenu.splice(1, 1);
}
return headerMenu;
};
}
export default FilesActionStore;

View File

@ -31,7 +31,7 @@ class FilesStore {
isLoaded = false;
isLoading = false;
viewAs = localStorage.getItem("viewAs") || "row";
viewAs = localStorage.getItem("viewAs") || "table";
dragging = false;
privacyInstructions = "https://www.onlyoffice.com/private-rooms.aspx";
isInit = false;
@ -992,7 +992,7 @@ class FilesStore {
}
get isHeaderVisible() {
return this.selection.length > 0 || this.selected !== "close";
return this.selection.length > 0;
}
get isHeaderIndeterminate() {
@ -1321,7 +1321,13 @@ class FilesStore {
}
//this.selected === "close" && this.setSelected("none");
this.setSelection(newSelection);
//need fo table view
const clearSelection = Object.values(
newSelection.reduce((item, n) => ((item[n.id] = n), item), {})
);
this.setSelection(clearSelection);
//}
};

View File

@ -2,8 +2,7 @@
"CustomNewDepartment": "{{groupCaption}} (Erstellung)",
"GroupAction": "Arbeit mit Gruppe",
"Members": "Mitglieder",
"Name": "Name",
"SearchAddedMembers": "Hinzugefügte Mitglieder suchen",
"SelectAction": "Auswählen",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' wurde erfolgreich gespeichert"
}
}

View File

@ -13,7 +13,6 @@
"EmailPopupHelper": "Die Haupt-E-Mail-Adresse wird für Benachrichtigungen und Zugriffswiederherstellung benutzt. <1> Sie können in dieser Domain eine neue E-Mail für den Benutzer erstellen und ein Einmalkennwort für die erste Anmeldung festlegen.</1>",
"FirstName": "Vorname",
"Message": "Chat",
"Phone": "Telefon",
"ProductsAndInstruments_Products": "Module",
"ProfileAction": "Arbeit mit dem Profil",
"ProfileTypePopupHelper": "Gäste haben eingeschränkten Zugriff auf einige Funktionen und Module auf dem Portal",

View File

@ -2,8 +2,7 @@
"CustomNewDepartment": "{{groupCaption}} (creation)",
"GroupAction": "Group action",
"Members": "Members",
"Name": "Name",
"SearchAddedMembers": "Search added members",
"SelectAction": "Select",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' has been saved successfully"
}
}

View File

@ -14,7 +14,6 @@
"EmailPopupHelper": "The main email is used for notifications and restoring access. <1> You can create a new email on this domain for the user and set a one-time password for the first login.</1>",
"FirstName": "First Name",
"Message": "Talk",
"Phone": "Phone",
"ProductsAndInstruments_Products": "Modules",
"ProfileAction": "Profile action",
"ProfileTypePopupHelper": "Guests have limited access to some portal features and modules",

View File

@ -2,7 +2,6 @@
"CustomNewDepartment": "{{groupCaption}} (création)",
"GroupAction": "Action de groupe",
"Members": "Membres",
"Name": "Nom",
"SearchAddedMembers": "Rechercher les membres ajoutés",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' a été sauvegardé avec succès"
}
}

View File

@ -13,7 +13,6 @@
"EmailPopupHelper": "L'adresse mail principal est utilisée pour les notifications et pour la restauration de l'accès. <1> Vous pouvez créer une nouvelle adresse mail sur ce domaine pour cet utilisateur et définir un mot de passe à usage unique pour la première connexion.",
"FirstName": "Prénom",
"Message": "Chat",
"Phone": "Téléphone",
"ProductsAndInstruments_Products": "Modules",
"ProfileAction": "Action sur le profil",
"ProfileTypePopupHelper": "Les invités ont l'accès limité aux certaines fonctions du portail et modules",

View File

@ -2,7 +2,6 @@
"CustomNewDepartment": "{{groupCaption}} (creazione)",
"GroupAction": "Azione di gruppo",
"Members": "Membri",
"Name": "Nome",
"SearchAddedMembers": "Cerca membri aggiunti",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' è stato salvato con successo"
}
}

View File

@ -13,7 +13,6 @@
"EmailPopupHelper": "L'email principale viene utilizzata per le notifiche e il ripristino dell'accesso. <1> Puoi creare una nuova email su questo dominio per l'utente e impostare una password monouso per il primo accesso.</1>",
"FirstName": "Nome",
"Message": "Parla",
"Phone": "Telefono",
"ProductsAndInstruments_Products": "Moduli",
"ProfileAction": "Azione del profilo",
"ProfileTypePopupHelper": "Gli ospiti hanno un tipo di accesso limitato a delle funzionalità, e nell'uso dei moduli di questo portale",

View File

@ -2,7 +2,6 @@
"CustomNewDepartment": "{{ກຸ່ມສົນທະນາ}} (ສ້າງ)",
"GroupAction": "ກຸ່ມດຳເນີນການ",
"Members": "ສະມາຊິກ",
"Name": "ຊື່",
"SearchAddedMembers": "ຄົ້ນຫາສະມາຊິກເພີ່ມ",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' ໄດ້ຖືກບັນທຶກ ສຳ ເລັດແລ້ວ"
}
}

View File

@ -13,7 +13,6 @@
"EmailPopupHelper": "ອີເມວຫຼັກແມ່ນໃຊ້ ສຳ ລັບການແຈ້ງເຕືອນແລະການກູ້ຄືນການເຂົ້າເຖິງ.<1> ທ່ານສາມາດສ້າງອີເມວໃໝ່ໃນໂດເມນນີ້ສຳລັບຜູ້ໃຊ້ແລະຕັ້ງລະຫັດຜ່ານໜຶ່ງຄັ້ງສຳລັບການເຂົ້າສູ່ລະບົບຄັ້ງທຳອິດ.</1>",
"FirstName": "ຊື່ແທ້",
"Message": "ເວົ້າ",
"Phone": "ໂທລະສັບ",
"ProductsAndInstruments_Products": "ໂມດູນ",
"ProfileAction": "ການດຳເນີນການໂປຣໄຟລ໌",
"ProfileTypePopupHelper": "ບຸກຄົນທົ່ວໄປຖືກຈຳກັດໃຫ້ເຂົ້າເຖິງຄຸນລັກສະນະບາງຢ່າງຂອງ Portal ແລະ ໂມດູນ",

View File

@ -2,7 +2,6 @@
"CustomNewDepartment": "{{groupCaption}} (criação)",
"GroupAction": "Ação em grupo",
"Members": "Membros",
"Name": "Nome",
"SearchAddedMembers": "Pesquisar membros adicionados",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' foi salvo com sucesso"
}
}

View File

@ -13,7 +13,6 @@
"EmailPopupHelper": "O e-mail principal é utilizado para notificações e restabelecimento do acesso. <1> Você pode criar um novo e-mail neste domínio para o usuário e definir uma senha única para o primeiro login.</1>",
"FirstName": "Primeiro nome",
"Message": "Bate-papo com outros usuários",
"Phone": "Telefone",
"ProductsAndInstruments_Products": "Módulos",
"ProfileAction": "Ação de perfil",
"ProfileTypePopupHelper": "Os hóspedes têm acesso limitado a alguns recursos e módulos do portal",

View File

@ -2,7 +2,6 @@
"CustomNewDepartment": "{{groupCaption}} (creare)",
"GroupAction": "Acțiune de grup",
"Members": "Membrii",
"Name": "Numele",
"SearchAddedMembers": "Căutare după membrii adăugați",
"SuccessSaveGroup": "{{groupCaption}} '{{ groupName }}' a fost salvat cu succes"
}
}

View File

@ -13,7 +13,6 @@
"EmailPopupHelper": "Adresa de email principală se folosește pentru trimiterea notificărilor și restabilirea accesului. <1> Puteți crea o nouă adresă de e-mail pentru utilizatorul pe acest domeniu și o parolă temporară unică cu care să conectează prima dată.</1>",
"FirstName": "Prenume",
"Message": "Chat",
"Phone": "Telefon",
"ProductsAndInstruments_Products": "Module",
"ProfileAction": "Acțiuni cu profil",
"ProfileTypePopupHelper": "Invitații au acces limitat la unele funcții și module ale portalului",

View File

@ -2,8 +2,7 @@
"CustomNewDepartment": "{{groupCaption}} (создание)",
"GroupAction": "Работа с группой",
"Members": "Участники",
"Name": "Название",
"SearchAddedMembers": "Поиск добавленных участников",
"SelectAction": "Выбрать",
"SuccessSaveGroup": "Успешное сохранение: {{groupCaption, lowercase}} '{{ groupName }}'"
}
}

View File

@ -14,7 +14,6 @@
"EmailPopupHelper": "Основной email нужен для восстановления доступа к порталу в случае потери пароля, а также для отправки оповещений. <1>Вы можете создать новый email на домене в качестве основного. В этом случае потребуется задать одноразовый пароль, чтобы пользователь смог войти на портал в первый раз.</1> Основной email можно использовать как логин при входе на портал.",
"FirstName": "Имя",
"Message": "Чат",
"Phone": "Телефон",
"ProductsAndInstruments_Products": "Модули",
"ProfileAction": "Работа с профилем",
"ProfileTypePopupHelper": "Гости имеют ограниченный доступ к некоторым функциям и модулям портала",

View File

@ -0,0 +1,152 @@
import React, { useCallback } from "react";
import { inject, observer } from "mobx-react";
import Link from "@appserver/components/link";
import LinkWithDropdown from "@appserver/components/link-with-dropdown";
import Avatar from "@appserver/components/avatar";
import config from "../../package.json";
import { combineUrl } from "@appserver/common/utils";
import { AppServerConfig } from "@appserver/common/constants";
export default function withContent(WrappedContent) {
const WithContent = (props) => {
const {
item,
selectGroup,
fetchProfile,
history,
checked,
selectUser,
deselectUser,
isAdmin,
} = props;
const { userName, mobilePhone, email, role, displayName, avatar } = item;
const onContentRowSelect = (checked, user) =>
checked ? selectUser(user) : deselectUser(user);
const checkedProps = isAdmin ? { checked } : {};
const element = (
<Avatar size="min" role={role} userName={displayName} source={avatar} />
);
const getFormattedGroups = () => {
let temp = [];
const groups = item.groups;
const linkColor = item.statusType === "pending" ? "#D0D5DA" : "#A3A9AE";
if (!groups) temp.push({ key: 0, label: "" });
groups &&
groups.map((group) =>
temp.push({
key: group.id,
label: group.name,
onClick: () => selectGroup(group.id),
})
);
if (temp.length <= 1) {
return (
<Link
isTextOverflow
containerMinWidth="120px"
containerWidth="15%"
type="action"
title={temp[0].label}
fontSize="12px"
fontWeight={400}
color={linkColor}
onClick={temp[0].onClick}
>
{temp[0].label}
</Link>
);
} else {
return (
<LinkWithDropdown
isTextOverflow
containerMinWidth="120px"
containerWidth="15%"
title={temp[0].label}
fontSize="12px"
fontWeight={400}
color={linkColor}
data={temp}
>
{temp[0].label}
</LinkWithDropdown>
);
}
};
const groups = getFormattedGroups();
const redirectToProfile = () => {
history.push(
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/view/${userName}`
)
);
};
const onUserNameClick = useCallback(
(e) => {
const timer = setTimeout(() => redirectToProfile(), 500);
e.preventDefault();
fetchProfile(userName).finally(() => {
clearTimeout(timer);
if (
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/view/${userName}`
) !== window.location.pathname
)
redirectToProfile();
});
},
[history, userName]
);
const onPhoneClick = () => window.open(`sms:${mobilePhone}`);
const onEmailClick = () => window.open(`mailto:${email}`);
return (
<WrappedContent
onContentRowSelect={onContentRowSelect}
onPhoneClick={onPhoneClick}
onEmailClick={onEmailClick}
onUserNameClick={onUserNameClick}
groups={groups}
checkedProps={checkedProps}
element={element}
isAdmin={isAdmin}
{...props}
/>
);
};
return inject(({ auth, peopleStore }, { item }) => {
const { isAdmin, userStore } = auth;
const { selectGroup } = peopleStore.selectedGroupStore;
const { getTargetUser } = peopleStore.targetUserStore;
const { selectionStore } = peopleStore;
const { selection, selectUser, deselectUser } = selectionStore;
return {
isAdmin,
currentUserId: userStore.user.id,
selectGroup,
fetchProfile: getTargetUser,
checked: selection.some((el) => el.id === item.id),
selectUser,
deselectUser,
};
})(observer(WithContent));
}

View File

@ -0,0 +1,292 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { combineUrl } from "@appserver/common/utils";
import { AppServerConfig, EmployeeStatus } from "@appserver/common/constants";
import { resendUserInvites } from "@appserver/common/api/people"; //TODO: Move to store action
import config from "../../package.json";
import { Trans, useTranslation } from "react-i18next";
import toastr from "studio/toastr";
export default function withContextOptions(WrappedComponent) {
const WithContextOptions = (props) => {
const {
isAdmin,
item,
history,
setDialogData,
closeDialogs,
setDeleteSelfProfileDialogVisible,
setChangePasswordDialogVisible,
setChangeEmailDialogVisible,
updateUserStatus,
setDeleteProfileDialogVisible,
fetchProfile,
} = props;
const { id, options, userName, email, mobilePhone, currentUserId } = item;
const isRefetchPeople = true; //TODO: why always true?
const { t } = useTranslation(["Home", "Translations"]);
const onEmailSentClick = () => {
window.open("mailto:" + email);
};
const onSendMessageClick = () => {
window.open(`sms:${mobilePhone}`);
};
const redirectToEdit = () => {
history.push(
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/edit/${userName}`
)
);
};
const onEditClick = () => {
const timer = setTimeout(() => redirectToEdit(), 500);
fetchProfile(userName).finally(() => {
clearTimeout(timer);
if (
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/edit/${userName}`
) !== window.location.pathname
)
redirectToEdit();
});
};
const toggleChangeEmailDialog = () => {
setDialogData({
email,
id,
});
setChangeEmailDialogVisible(true);
};
const toggleChangePasswordDialog = () => {
setDialogData({
email,
});
setChangePasswordDialogVisible(true);
};
const toggleDeleteSelfProfileDialog = () => {
closeDialogs();
setDialogData({
email,
});
setDeleteSelfProfileDialogVisible(true);
};
const toggleDeleteProfileEverDialog = () => {
closeDialogs();
setDialogData({
id,
displayName,
userName,
});
setDeleteProfileDialogVisible(true);
};
const onDisableClick = (e) => {
//onLoading(true);
updateUserStatus(EmployeeStatus.Disabled, [id], isRefetchPeople)
.then(() => toastr.success(t("Translations:SuccessChangeUserStatus")))
.catch((error) => toastr.error(error));
//.finally(() => onLoading(false));
};
const onEnableClick = () => {
//onLoading(true);
updateUserStatus(EmployeeStatus.Active, [id], isRefetchPeople)
.then(() => toastr.success(t("Translations:SuccessChangeUserStatus")))
.catch((error) => toastr.error(error));
//.finally(() => onLoading(false));
};
const onReassignDataClick = () => {
history.push(
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/reassign/${userName}`
)
);
};
const onDeletePersonalDataClick = () => {
toastr.success(t("Translations:SuccessDeletePersonalData"));
};
const onInviteAgainClick = () => {
//onLoading(true);
resendUserInvites([id])
.then(() =>
toastr.success(
<Trans
i18nKey="MessageEmailActivationInstuctionsSentOnEmail"
ns="Home"
t={t}
>
The email activation instructions have been sent to the
<strong>{{ email: email }}</strong> email address
</Trans>
)
)
.catch((error) => toastr.error(error));
//.finally(() => onLoading(false));
};
const getUserContextOptions = () => {
const contextMenu = options.map((option) => {
switch (option) {
case "send-email":
return {
key: option,
label: t("LblSendEmail"),
"data-id": id,
onClick: onEmailSentClick,
};
case "send-message":
return {
key: option,
label: t("LblSendMessage"),
"data-id": id,
onClick: onSendMessageClick,
};
case "separator":
return { key: option, isSeparator: true };
case "edit":
return {
key: option,
label: t("Common:EditButton"),
"data-id": id,
onClick: onEditClick,
};
case "change-password":
return {
key: option,
label: t("Translations:PasswordChangeButton"),
"data-id": id,
onClick: toggleChangePasswordDialog,
};
case "change-email":
return {
key: option,
label: t("Translations:EmailChangeButton"),
"data-id": id,
onClick: toggleChangeEmailDialog,
};
case "delete-self-profile":
return {
key: option,
label: t("Translations:DeleteSelfProfile"),
"data-id": id,
onClick: toggleDeleteSelfProfileDialog,
};
case "disable":
return {
key: option,
label: t("Translations:DisableUserButton"),
"data-id": id,
onClick: onDisableClick,
};
case "enable":
return {
key: option,
label: t("Translations:EnableUserButton"),
"data-id": id,
onClick: onEnableClick,
};
case "reassign-data":
return {
key: option,
label: t("Translations:ReassignData"),
"data-id": id,
onClick: onReassignDataClick,
};
case "delete-personal-data":
return {
key: option,
label: t("Translations:RemoveData"),
"data-id": id,
onClick: onDeletePersonalDataClick,
};
case "delete-profile":
return {
key: option,
label: t("Translations:DeleteSelfProfile"),
"data-id": id,
onClick: toggleDeleteProfileEverDialog,
};
case "invite-again":
return {
key: option,
label: t("LblInviteAgain"),
"data-id": id,
onClick: onInviteAgainClick,
};
default:
break;
}
return undefined;
});
return contextMenu;
};
const showContextMenu = options && options.length > 0;
const contextOptionsProps =
(isAdmin && showContextMenu) || (showContextMenu && id === currentUserId)
? {
contextOptions: getUserContextOptions(),
}
: {};
return (
<WrappedComponent contextOptionsProps={contextOptionsProps} {...props} />
);
};
return inject(({ auth, peopleStore }) => {
const { isAdmin } = auth;
const { dialogStore, targetUserStore, usersStore } = peopleStore;
const { getTargetUser } = targetUserStore;
const { updateUserStatus } = usersStore;
const {
setDialogData,
closeDialogs,
setDeleteSelfProfileDialogVisible,
setChangePasswordDialogVisible,
setChangeEmailDialogVisible,
setDeleteProfileDialogVisible,
} = dialogStore;
return {
isAdmin,
fetchProfile: getTargetUser,
setDialogData,
closeDialogs,
setDeleteSelfProfileDialogVisible,
setChangePasswordDialogVisible,
setChangeEmailDialogVisible,
updateUserStatus,
setDeleteProfileDialogVisible,
};
})(observer(WithContextOptions));
}

View File

@ -96,7 +96,7 @@ const SectionBodyContent = ({
isLoaded,
tReady,
}) => {
const { t, i18n } = useTranslation(["GroupAction", "Translations"]);
const { t, i18n } = useTranslation(["GroupAction", "Translations", "Common"]);
const [inLoading, setInLoading] = useState(false);
const [isHeadSelectorOpen, setIsHeadSelectorOpen] = useState(false);
@ -242,7 +242,7 @@ const SectionBodyContent = ({
hasError={!!nameError}
errorMessage={nameError}
isVertical={true}
labelText={t("Name")}
labelText={t("Common:Name")}
>
<TextInput
id="group-name"

View File

@ -5,7 +5,13 @@ import {
ChangePasswordDialog,
DeleteSelfProfileDialog,
DeleteProfileEverDialog,
ChangeUserTypeDialog,
ChangeUserStatusDialog,
SendInviteDialog,
DeleteUsersDialog,
InviteDialog,
} from "../../../../components/dialogs";
import { EmployeeType, EmployeeStatus } from "@appserver/common/constants";
const Dialogs = ({
changeEmail,
@ -14,7 +20,13 @@ const Dialogs = ({
deleteProfileEver,
data,
closeDialogs,
filter,
employeeDialogVisible,
guestDialogVisible,
activeDialogVisible,
disableDialogVisible,
sendInviteDialogVisible,
deleteDialogVisible,
invitationDialogVisible,
}) => {
return (
<>
@ -46,15 +58,90 @@ const Dialogs = ({
user={data}
/>
)}
{employeeDialogVisible && (
<ChangeUserTypeDialog
visible={employeeDialogVisible}
onClose={closeDialogs}
userType={EmployeeType.User}
/>
)}
{guestDialogVisible && (
<ChangeUserTypeDialog
visible={guestDialogVisible}
onClose={closeDialogs}
userType={EmployeeType.Guest}
/>
)}
{activeDialogVisible && (
<ChangeUserStatusDialog
visible={activeDialogVisible}
onClose={closeDialogs}
userStatus={EmployeeStatus.Active}
/>
)}
{disableDialogVisible && (
<ChangeUserStatusDialog
visible={disableDialogVisible}
onClose={closeDialogs}
userStatus={EmployeeStatus.Disabled}
/>
)}
{sendInviteDialogVisible && (
<SendInviteDialog
visible={sendInviteDialogVisible}
onClose={closeDialogs}
/>
)}
{deleteDialogVisible && (
<DeleteUsersDialog
visible={deleteDialogVisible}
onClose={closeDialogs}
/>
)}
{invitationDialogVisible && (
<InviteDialog
visible={invitationDialogVisible}
onClose={closeDialogs}
onCloseButton={closeDialogs}
/>
)}
</>
);
};
export default inject(({ peopleStore }) => ({
changeEmail: peopleStore.dialogStore.changeEmail,
changePassword: peopleStore.dialogStore.changePassword,
deleteSelfProfile: peopleStore.dialogStore.deleteSelfProfile,
deleteProfileEver: peopleStore.dialogStore.deleteProfileEver,
data: peopleStore.dialogStore.data,
closeDialogs: peopleStore.dialogStore.closeDialogs,
}))(observer(Dialogs));
export default inject(({ peopleStore }) => {
const {
changeEmail,
changePassword,
deleteSelfProfile,
deleteProfileEver,
data,
closeDialogs,
employeeDialogVisible,
guestDialogVisible,
activeDialogVisible,
disableDialogVisible,
sendInviteDialogVisible,
deleteDialogVisible,
invitationDialogVisible,
} = peopleStore.dialogStore;
return {
changeEmail,
changePassword,
deleteSelfProfile,
deleteProfileEver,
data,
closeDialogs,
employeeDialogVisible,
guestDialogVisible,
activeDialogVisible,
disableDialogVisible,
sendInviteDialogVisible,
deleteDialogVisible,
invitationDialogVisible,
};
})(observer(Dialogs));

View File

@ -0,0 +1,43 @@
import React, { useEffect } from "react";
import { inject, observer } from "mobx-react";
import RowContainer from "@appserver/components/row-container";
import SimpleUserRow from "./SimpleUserRow";
import EmptyScreen from "../EmptyScreen";
const PeopleRowContainer = ({
peopleList,
sectionWidth,
viewAs,
setViewAs,
}) => {
useEffect(() => {
if (viewAs !== "table" && viewAs !== "row") return;
if (sectionWidth < 1025) {
viewAs !== "row" && setViewAs("row");
} else {
viewAs !== "table" && setViewAs("table");
}
}, [sectionWidth]);
return peopleList.length > 0 ? (
<RowContainer className="people-row-container" useReactWindow={false}>
{peopleList.map((item) => (
<SimpleUserRow key={item.id} item={item} sectionWidth={sectionWidth} />
))}
</RowContainer>
) : (
<EmptyScreen />
);
};
export default inject(({ peopleStore }) => {
const { usersStore, viewAs, setViewAs } = peopleStore;
const { peopleList } = usersStore;
return {
peopleList,
viewAs,
setViewAs,
};
})(observer(PeopleRowContainer));

View File

@ -0,0 +1,33 @@
import React from "react";
import Row from "@appserver/components/row";
import UserContent from "./userContent";
import { withRouter } from "react-router";
import withContextOptions from "../../../../../HOCs/withContextOptions";
import withContent from "../../../../../HOCs/withContent";
const SimpleUserRow = (props) => {
const {
item,
sectionWidth,
contextOptionsProps,
checkedProps,
onContentRowSelect,
element,
} = props;
return (
<Row
key={item.id}
data={item}
element={element}
onSelect={onContentRowSelect}
{...checkedProps}
{...contextOptionsProps}
sectionWidth={sectionWidth}
>
<UserContent {...props} />
</Row>
);
};
export default withRouter(withContextOptions(withContent(SimpleUserRow)));

View File

@ -0,0 +1,111 @@
import React from "react";
import { withRouter } from "react-router";
import styled from "styled-components";
import RowContent from "@appserver/components/row-content";
import Link from "@appserver/components/link";
import Text from "@appserver/components/text";
import Box from "@appserver/components/box";
import commonIconsStyles from "@appserver/components/utils/common-icons-style";
import SendClockIcon from "../../../../../../public/images/send.clock.react.svg";
import CatalogSpamIcon from "../../../../../../public/images/catalog.spam.react.svg";
const StyledSendClockIcon = styled(SendClockIcon)`
${commonIconsStyles}
path {
fill: #3b72a7;
}
`;
const StyledCatalogSpamIcon = styled(CatalogSpamIcon)`
${commonIconsStyles}
path {
fill: #3b72a7;
}
`;
const UserContent = ({
item,
sectionWidth,
onPhoneClick,
onEmailClick,
onUserNameClick,
groups,
}) => {
const { userName, displayName, title, mobilePhone, email, statusType } = item;
const nameColor = statusType === "pending" ? "#A3A9AE" : "#333333";
const sideInfoColor = statusType === "pending" ? "#D0D5DA" : "#A3A9AE";
return (
<RowContent
sideColor={sideInfoColor}
sectionWidth={sectionWidth}
nameColor={nameColor}
sideInfoColor={sideInfoColor}
>
<Link
containerWidth="28%"
type="page"
href={`/products/people/view/${userName}`}
title={displayName}
fontWeight={600}
onClick={onUserNameClick}
fontSize="15px"
color={nameColor}
isTextOverflow={true}
>
{displayName}
</Link>
<>
{statusType === "pending" && <StyledSendClockIcon size="small" />}
{statusType === "disabled" && <StyledCatalogSpamIcon size="small" />}
</>
{title ? (
<Text
containerMinWidth="120px"
containerWidth="20%"
as="div"
color={sideInfoColor}
fontSize="12px"
fontWeight={600}
title={title}
truncate={true}
>
{title}
</Text>
) : (
<Box containerMinWidth="120px" containerWidth="20%"></Box>
)}
{groups}
<Link
containerMinWidth="60px"
containerWidth="15%"
type="page"
title={mobilePhone}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
onClick={onPhoneClick}
isTextOverflow={true}
>
{mobilePhone}
</Link>
<Link
containerMinWidth="140px"
containerWidth="17%"
type="page"
title={email}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
onClick={onEmailClick}
isTextOverflow={true}
>
{email}
</Link>
</RowContent>
);
};
export default withRouter(UserContent);

View File

@ -1,330 +0,0 @@
import React from "react";
import Row from "@appserver/components/row";
import Avatar from "@appserver/components/avatar";
import UserContent from "./userContent";
import { inject, observer } from "mobx-react";
import { Trans, useTranslation } from "react-i18next";
import toastr from "studio/toastr";
import { AppServerConfig, EmployeeStatus } from "@appserver/common/constants";
import { resendUserInvites } from "@appserver/common/api/people"; //TODO: Move to store action
import { withRouter } from "react-router";
import config from "../../../../../package.json";
import { combineUrl } from "@appserver/common/utils";
const SimpleUserRow = ({
person,
sectionWidth,
checked,
isAdmin,
isMobile,
selectUser,
selectGroup,
deselectUser,
setChangeEmailDialogVisible,
setChangePasswordDialogVisible,
setDeleteProfileDialogVisible,
setDeleteSelfProfileDialogVisible,
setDialogData,
closeDialogs,
updateUserStatus,
history,
fetchProfile,
}) => {
const { t } = useTranslation(["Home", "Translations"]);
const isRefetchPeople = true;
const {
email,
role,
displayName,
avatar,
id,
status,
mobilePhone,
options,
userName,
currentUserId,
} = person;
const onContentRowSelect = (checked, user) =>
checked ? selectUser(user) : deselectUser(user);
const onEmailSentClick = () => {
window.open("mailto:" + email);
};
const onSendMessageClick = () => {
window.open(`sms:${mobilePhone}`);
};
const redirectToEdit = () => {
history.push(
combineUrl(AppServerConfig.proxyURL, config.homepage, `/edit/${userName}`)
);
};
const onEditClick = () => {
const timer = setTimeout(() => redirectToEdit(), 500);
fetchProfile(userName).finally(() => {
clearTimeout(timer);
if (
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/edit/${userName}`
) !== window.location.pathname
)
redirectToEdit();
});
};
const toggleChangeEmailDialog = () => {
setDialogData({
email,
id,
});
setChangeEmailDialogVisible(true);
};
const toggleChangePasswordDialog = () => {
setDialogData({
email,
});
setChangePasswordDialogVisible(true);
};
const toggleDeleteSelfProfileDialog = (e) => {
closeDialogs();
setDialogData({
email,
});
setDeleteSelfProfileDialogVisible(true);
};
const toggleDeleteProfileEverDialog = (e) => {
closeDialogs();
setDialogData({
id,
displayName,
userName,
});
setDeleteProfileDialogVisible(true);
};
const onDisableClick = (e) => {
//onLoading(true);
updateUserStatus(EmployeeStatus.Disabled, [id], isRefetchPeople)
.then(() => toastr.success(t("Translations:SuccessChangeUserStatus")))
.catch((error) => toastr.error(error));
//.finally(() => onLoading(false));
};
const onEnableClick = (e) => {
//onLoading(true);
updateUserStatus(EmployeeStatus.Active, [id], isRefetchPeople)
.then(() => toastr.success(t("Translations:SuccessChangeUserStatus")))
.catch((error) => toastr.error(error));
//.finally(() => onLoading(false));
};
const onReassignDataClick = (e) => {
history.push(
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/reassign/${userName}`
)
);
};
const onDeletePersonalDataClick = (e) => {
toastr.success(t("Translations:SuccessDeletePersonalData"));
};
const onInviteAgainClick = () => {
//onLoading(true);
resendUserInvites([id])
.then(() =>
toastr.success(
<Trans
i18nKey="MessageEmailActivationInstuctionsSentOnEmail"
ns="Home"
t={t}
>
The email activation instructions have been sent to the
<strong>{{ email: email }}</strong> email address
</Trans>
)
)
.catch((error) => toastr.error(error));
//.finally(() => onLoading(false));
};
const getUserContextOptions = (options, id) => {
const contextMenu = options.map((option) => {
switch (option) {
case "send-email":
return {
key: option,
label: t("LblSendEmail"),
"data-id": id,
onClick: onEmailSentClick,
};
case "send-message":
return {
key: option,
label: t("LblSendMessage"),
"data-id": id,
onClick: onSendMessageClick,
};
case "separator":
return { key: option, isSeparator: true };
case "edit":
return {
key: option,
label: t("Common:EditButton"),
"data-id": id,
onClick: onEditClick,
};
case "change-password":
return {
key: option,
label: t("Translations:PasswordChangeButton"),
"data-id": id,
onClick: toggleChangePasswordDialog,
};
case "change-email":
return {
key: option,
label: t("Translations:EmailChangeButton"),
"data-id": id,
onClick: toggleChangeEmailDialog,
};
case "delete-self-profile":
return {
key: option,
label: t("Translations:DeleteSelfProfile"),
"data-id": id,
onClick: toggleDeleteSelfProfileDialog,
};
case "disable":
return {
key: option,
label: t("Translations:DisableUserButton"),
"data-id": id,
onClick: onDisableClick,
};
case "enable":
return {
key: option,
label: t("Translations:EnableUserButton"),
"data-id": id,
onClick: onEnableClick,
};
case "reassign-data":
return {
key: option,
label: t("Translations:ReassignData"),
"data-id": id,
onClick: onReassignDataClick,
};
case "delete-personal-data":
return {
key: option,
label: t("Translations:RemoveData"),
"data-id": id,
onClick: onDeletePersonalDataClick,
};
case "delete-profile":
return {
key: option,
label: t("Translations:DeleteSelfProfile"),
"data-id": id,
onClick: toggleDeleteProfileEverDialog,
};
case "invite-again":
return {
key: option,
label: t("LblInviteAgain"),
"data-id": id,
onClick: onInviteAgainClick,
};
default:
break;
}
return undefined;
});
return contextMenu;
};
const showContextMenu = options && options.length > 0;
const checkedProps = isAdmin ? { checked } : {};
const element = (
<Avatar size="min" role={role} userName={displayName} source={avatar} />
);
const contextOptionsProps =
(isAdmin && showContextMenu) || (showContextMenu && id === currentUserId)
? {
contextOptions: getUserContextOptions(options, id),
}
: {};
return (
<Row
key={id}
status={status}
data={person}
element={element}
onSelect={onContentRowSelect}
{...checkedProps}
{...contextOptionsProps}
//needForUpdate={this.needForUpdate}
sectionWidth={sectionWidth}
>
<UserContent
isMobile={isMobile}
//widthProp={widthProp}
user={person}
history={history}
selectGroup={selectGroup}
sectionWidth={sectionWidth}
fetchProfile={fetchProfile}
/>
</Row>
);
};
export default withRouter(
inject(({ auth, peopleStore }, { person }) => {
return {
isAdmin: auth.isAdmin,
currentUserId: auth.userStore.user.id,
checked: peopleStore.selectionStore.selection.some(
(el) => el.id === person.id
),
selectUser: peopleStore.selectionStore.selectUser,
deselectUser: peopleStore.selectionStore.deselectUser,
selectGroup: peopleStore.selectedGroupStore.selectGroup,
setChangeEmailDialogVisible:
peopleStore.dialogStore.setChangeEmailDialogVisible,
setChangePasswordDialogVisible:
peopleStore.dialogStore.setChangePasswordDialogVisible,
setDeleteSelfProfileDialogVisible:
peopleStore.dialogStore.setDeleteSelfProfileDialogVisible,
setDeleteProfileDialogVisible:
peopleStore.dialogStore.setDeleteProfileDialogVisible,
setDialogData: peopleStore.dialogStore.setDialogData,
closeDialogs: peopleStore.dialogStore.closeDialogs,
updateUserStatus: peopleStore.usersStore.updateUserStatus,
fetchProfile: peopleStore.targetUserStore.getTargetUser,
};
})(observer(SimpleUserRow))
);

View File

@ -0,0 +1,43 @@
import React, { useEffect, useRef } from "react";
import TableContainer from "@appserver/components/table-container";
import { inject, observer } from "mobx-react";
import TableRow from "./TableRow";
import TableHeader from "./TableHeader";
import TableBody from "@appserver/components/table-container/TableBody";
import EmptyScreen from "../EmptyScreen";
const Table = ({ peopleList, sectionWidth, viewAs, setViewAs }) => {
const ref = useRef(null);
useEffect(() => {
if (sectionWidth < 1025) {
viewAs !== "row" && setViewAs("row");
} else {
viewAs !== "table" && setViewAs("table");
}
}, [sectionWidth]);
return peopleList.length > 0 ? (
<TableContainer forwardedRef={ref}>
<TableHeader sectionWidth={sectionWidth} containerRef={ref} />
<TableBody>
{peopleList.map((item) => (
<TableRow key={item.id} item={item} />
))}
</TableBody>
</TableContainer>
) : (
<EmptyScreen />
);
};
export default inject(({ peopleStore }) => {
const { usersStore, viewAs, setViewAs } = peopleStore;
const { peopleList } = usersStore;
return {
peopleList,
viewAs,
setViewAs,
};
})(observer(Table));

View File

@ -0,0 +1,231 @@
import React from "react";
import TableHeader from "@appserver/components/table-container/TableHeader";
import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import DropDownItem from "@appserver/components/drop-down-item";
const TABLE_COLUMNS = "peopleTableColumns";
class PeopleTableHeader extends React.Component {
constructor(props) {
super(props);
const { t } = props;
const defaultColumns = [
{
key: "Name",
title: t("Common:Name"),
resizable: true,
enable: true,
default: true,
sortBy: "AZ",
active: true,
minWidth: 180,
onClick: this.onFilter,
onIconClick: this.onIconClick,
},
{
key: "Department",
title: t("Common:Department"),
enable: true,
resizable: true,
onChange: this.onColumnChange,
},
{
key: "Role",
title: t("Common:Role"),
enable: true,
resizable: true,
onChange: this.onColumnChange,
},
{
key: "Phone",
title: t("Common:Phone"),
enable: true,
resizable: true,
onChange: this.onColumnChange,
},
{
key: "Mail",
title: t("Common:Mail"),
enable: true,
resizable: true,
onChange: this.onColumnChange,
},
];
const columns = this.getColumns(defaultColumns);
this.state = { columns };
}
getColumns = (defaultColumns) => {
const storageColumns = localStorage.getItem(TABLE_COLUMNS);
const columns = [];
if (storageColumns) {
const splitColumns = storageColumns.split(",");
for (let col of defaultColumns) {
const column = splitColumns.find((key) => key === col.key);
column ? (col.enable = true) : (col.enable = false);
columns.push(col);
}
return columns;
} else {
return defaultColumns;
}
};
onColumnChange = (key, e) => {
const { columns } = this.state;
const columnIndex = columns.findIndex((c) => c.key === key);
if (columnIndex === -1) return;
columns[columnIndex].enable = !columns[columnIndex].enable;
this.setState({ columns });
const tableColumns = columns.map((c) => c.enable && c.key);
localStorage.setItem(TABLE_COLUMNS, tableColumns);
};
onFilter = () => {
const { filter, setIsLoading, fetchPeople } = this.props;
const newFilter = filter.clone();
if (newFilter.sortBy === "lastname") {
newFilter.sortBy = "firstname";
} else {
newFilter.sortBy = "lastname";
}
setIsLoading(true);
fetchPeople(newFilter).finally(() => setIsLoading(false));
};
onIconClick = () => {
const { filter, setIsLoading, fetchPeople } = this.props;
const newFilter = filter.clone();
if (newFilter.sortOrder === "ascending") {
newFilter.sortOrder = "descending";
} else {
newFilter.sortOrder = "ascending";
}
setIsLoading(true);
fetchPeople(newFilter).finally(() => setIsLoading(false));
};
onChange = (checked) => {
this.props.setSelected(checked ? "all" : "none");
};
onSelect = (e) => {
const key = e.currentTarget.dataset.key;
this.props.setSelected(key);
};
setSelected = (checked) => {
const { isAdmin, setSelected } = this.props;
if (isAdmin) {
setSelected && setSelected(checked ? "all" : "none");
}
};
render() {
const { columns } = this.state;
const {
t,
containerRef,
isHeaderVisible,
isHeaderChecked,
isHeaderIndeterminate,
getHeaderMenu,
filter,
sectionWidth,
isAdmin,
} = this.props;
const { sortOrder } = filter;
const checkboxOptions = (
<>
<DropDownItem
label={t("Common:Active")}
data-key="active"
onClick={this.onSelect}
/>
<DropDownItem
label={t("Translations:DisabledEmployeeStatus")}
data-key="disabled"
onClick={this.onSelect}
/>
<DropDownItem
label={t("LblInvited")}
data-key="invited"
onClick={this.onSelect}
/>
</>
);
return (
<TableHeader
hasAccess={isAdmin}
checkboxSize="48px"
sorted={sortOrder === "descending"}
setSelected={this.setSelected}
containerRef={containerRef}
columns={columns}
columnStorageName="peopleColumnsSize"
sectionWidth={sectionWidth}
isHeaderVisible={isHeaderVisible}
checkboxOptions={checkboxOptions}
onChange={this.onChange}
isChecked={isHeaderChecked}
isIndeterminate={isHeaderIndeterminate}
headerMenu={getHeaderMenu(t)}
checkboxMargin="12px"
/>
);
}
}
export default inject(({ auth, peopleStore }) => {
const {
selectionStore,
headerMenuStore,
filterStore,
usersStore,
loadingStore,
getHeaderMenu,
} = peopleStore;
const { setSelected } = selectionStore;
const {
isHeaderVisible,
isHeaderChecked,
isHeaderIndeterminate,
} = headerMenuStore;
const { filter } = filterStore;
const { getUsersList } = usersStore;
const { setIsLoading } = loadingStore;
return {
isAdmin: auth.isAdmin,
setSelected,
isHeaderVisible,
filter,
fetchPeople: getUsersList,
setIsLoading,
getHeaderMenu,
isHeaderChecked,
isHeaderIndeterminate,
};
})(
withTranslation(["Home", "Common", "Translations"])(
observer(PeopleTableHeader)
)
);

View File

@ -0,0 +1,110 @@
import React from "react";
import { withRouter } from "react-router";
import TableRow from "@appserver/components/table-container/TableRow";
import TableCell from "@appserver/components/table-container/TableCell";
import withContextOptions from "../../../../../HOCs/withContextOptions";
import withContent from "../../../../../HOCs/withContent";
import Link from "@appserver/components/link";
import Text from "@appserver/components/text";
import styled from "styled-components";
const StyledPeopleRow = styled(TableRow)`
.table-container_cell {
height: 46px;
max-height: 46px;
}
.table-container_row-checkbox-wrapper {
padding-left: 4px;
.table-container_row-checkbox {
margin-left: 8px;
}
}
`;
const PeopleTableRow = (props) => {
const {
item,
contextOptionsProps,
element,
checkedProps,
onContentRowSelect,
groups,
onEmailClick,
onUserNameClick,
isAdmin,
} = props;
const { displayName, email, role, statusType, userName } = item;
const nameColor = statusType === "pending" ? "#A3A9AE" : "#333333";
const sideInfoColor = statusType === "pending" ? "#D0D5DA" : "#A3A9AE";
return (
<StyledPeopleRow
key={item.id}
item={item}
element={element}
onContentSelect={onContentRowSelect}
hasAccess={isAdmin}
{...contextOptionsProps}
{...checkedProps}
>
<TableCell>
<Link
type="page"
title={displayName}
fontWeight="600"
fontSize="15px"
color={nameColor}
isTextOverflow
href={`/products/people/view/${userName}`}
onClick={onUserNameClick}
>
{displayName}
</Link>
</TableCell>
<TableCell>{groups}</TableCell>
<TableCell>
<Text
type="page"
title={role}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
truncate
>
{role}
</Text>
</TableCell>
<TableCell>
<Text
style={{ display: "none" }} //TODO:
type="page"
title={role}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
truncate
>
Phone
</Text>
</TableCell>
<TableCell>
<Link
type="page"
title={email}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
onClick={onEmailClick}
isTextOverflow
>
{email}
</Link>
</TableCell>
</StyledPeopleRow>
);
};
export default withRouter(withContextOptions(withContent(PeopleTableRow)));

View File

@ -1,53 +1,39 @@
import React from "react";
import RowContainer from "@appserver/components/row-container";
import { Consumer } from "@appserver/components/utils/context";
import { withTranslation } from "react-i18next";
//import toastr from "studio/toastr";
import EmptyScreen from "./EmptyScreen";
import { inject, observer } from "mobx-react";
import SimpleUserRow from "./SimpleUserRow";
import Dialogs from "./Dialogs";
import { isMobile } from "react-device-detect";
import withLoader from "../../../../HOCs/withLoader";
import Loaders from "@appserver/common/components/Loaders";
import { withTranslation } from "react-i18next";
import withLoader from "../../../../HOCs/withLoader";
import PeopleRowContainer from "./RowView/PeopleRowContainer";
import TableView from "./TableView/TableContainer";
import { Consumer } from "@appserver/components/utils/context";
const SectionBodyContent = ({ peopleList, tReady }) => {
return peopleList.length > 0 ? (
<>
class SectionBodyContent extends React.Component {
render() {
const { tReady, viewAs } = this.props;
return (
<Consumer>
{(context) => (
<RowContainer
className="people-row-container"
useReactWindow={false}
tReady={tReady}
>
{peopleList.map((person) => (
<SimpleUserRow
key={person.id}
person={person}
sectionWidth={context.sectionWidth}
isMobile={isMobile}
/>
))}
</RowContainer>
)}
{(context) =>
viewAs === "table" ? (
<TableView sectionWidth={context.sectionWidth} tReady={tReady} />
) : (
<PeopleRowContainer
sectionWidth={context.sectionWidth}
tReady={tReady}
/>
)
}
</Consumer>
<Dialogs />
</>
) : (
<EmptyScreen />
);
};
);
}
}
export default inject(({ auth, peopleStore }) => ({
isLoaded: auth.isLoaded,
isRefresh: peopleStore.isRefresh,
peopleList: peopleStore.usersStore.peopleList,
isLoading: peopleStore.isLoading,
}))(
withTranslation("Home")(
export default inject(({ peopleStore }) => {
const { viewAs, setViewAs } = peopleStore;
return { viewAs, setViewAs };
})(
withTranslation(["Home", "Common", "Translations"])(
withLoader(observer(SectionBodyContent))(
<Loaders.Rows isRectangle={false} />
)

View File

@ -1,199 +0,0 @@
import React, { useCallback } from "react";
import { withRouter } from "react-router";
import styled from "styled-components";
import RowContent from "@appserver/components/row-content";
import Link from "@appserver/components/link";
import LinkWithDropdown from "@appserver/components/link-with-dropdown";
import Text from "@appserver/components/text";
import Box from "@appserver/components/box";
import commonIconsStyles from "@appserver/components/utils/common-icons-style";
import SendClockIcon from "../../../../../public/images/send.clock.react.svg";
import CatalogSpamIcon from "../../../../../public/images/catalog.spam.react.svg";
import config from "../../../../../package.json";
import { combineUrl } from "@appserver/common/utils";
import { AppServerConfig } from "@appserver/common/constants";
const StyledSendClockIcon = styled(SendClockIcon)`
${commonIconsStyles}
path {
fill: #3b72a7;
}
`;
const StyledCatalogSpamIcon = styled(CatalogSpamIcon)`
${commonIconsStyles}
path {
fill: #3b72a7;
}
`;
const getFormattedGroups = (user, selectGroup) => {
let temp = [];
const groups = user.groups;
const linkColor = user.statusType === "pending" ? "#D0D5DA" : "#A3A9AE";
if (!groups) temp.push({ key: 0, label: "" });
groups &&
groups.map((group) =>
temp.push({
key: group.id,
label: group.name,
onClick: () => selectGroup(group.id),
})
);
if (temp.length <= 1) {
return (
<Link
isTextOverflow={true}
containerMinWidth="120px"
containerWidth="15%"
type="action"
title={temp[0].label}
fontSize="12px"
fontWeight={400}
color={linkColor}
onClick={temp[0].onClick}
>
{temp[0].label}
</Link>
);
} else {
return (
<LinkWithDropdown
isTextOverflow={true}
containerMinWidth="120px"
containerWidth="15%"
title={temp[0].label}
fontSize="12px"
fontWeight={400}
color={linkColor}
data={temp}
>
{temp[0].label}
</LinkWithDropdown>
);
}
};
const UserContent = ({
user,
history,
selectGroup,
//widthProp,
isMobile,
sectionWidth,
fetchProfile,
}) => {
const { userName, displayName, title, mobilePhone, email, statusType } = user;
const groups = getFormattedGroups(user, selectGroup);
const redirectToProfile = () => {
history.push(
combineUrl(AppServerConfig.proxyURL, config.homepage, `/view/${userName}`)
);
};
const onUserNameClick = useCallback(
(e) => {
const timer = setTimeout(() => redirectToProfile(), 500);
e.preventDefault();
fetchProfile(userName).finally(() => {
clearTimeout(timer);
if (
combineUrl(
AppServerConfig.proxyURL,
config.homepage,
`/view/${userName}`
) !== window.location.pathname
)
redirectToProfile();
});
},
[history, userName]
);
const onPhoneClick = useCallback(() => window.open(`sms:${mobilePhone}`), [
mobilePhone,
]);
const onEmailClick = useCallback(() => window.open(`mailto:${email}`), [
email,
]);
const nameColor = statusType === "pending" ? "#A3A9AE" : "#333333";
const sideInfoColor = statusType === "pending" ? "#D0D5DA" : "#A3A9AE";
return (
<RowContent
//widthProp={widthProp}
isMobile={isMobile}
sideColor={sideInfoColor}
sectionWidth={sectionWidth}
>
<Link
containerWidth="28%"
type="page"
href={`/products/people/view/${userName}`}
title={displayName}
fontWeight={600}
onClick={onUserNameClick}
fontSize="15px"
color={nameColor}
isTextOverflow={true}
>
{displayName}
</Link>
<>
{statusType === "pending" && <StyledSendClockIcon size="small" />}
{statusType === "disabled" && <StyledCatalogSpamIcon size="small" />}
</>
{title ? (
<Text
containerMinWidth="120px"
containerWidth="20%"
as="div"
color={sideInfoColor}
fontSize="12px"
fontWeight={600}
title={title}
truncate={true}
>
{title}
</Text>
) : (
<Box containerMinWidth="120px" containerWidth="20%"></Box>
)}
{groups}
<Link
containerMinWidth="60px"
containerWidth="15%"
type="page"
title={mobilePhone}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
onClick={onPhoneClick}
isTextOverflow={true}
>
{mobilePhone}
</Link>
<Link
containerMinWidth="140px"
containerWidth="17%"
type="page"
title={email}
fontSize="12px"
fontWeight={400}
color={sideInfoColor}
onClick={onEmailClick}
isTextOverflow={true}
>
{email}
</Link>
</RowContent>
);
};
export default withRouter(UserContent);

View File

@ -227,7 +227,7 @@ class SectionFilterContent extends React.Component {
render() {
const selectedFilterData = this.getSelectedFilterData();
const { t, isLoaded, sectionWidth, tReady } = this.props;
const { t, isLoaded, sectionWidth, tReady, viewAs } = this.props;
return isLoaded && tReady ? (
<FilterInput
@ -241,6 +241,8 @@ class SectionFilterContent extends React.Component {
placeholder={t("Common:Search")}
contextMenuHeader={t("Common:AddFilter")}
isMobile={isMobileOnly}
viewAs={viewAs}
viewSelectorVisible={false}
/>
) : (
<Loaders.Filter />
@ -250,7 +252,13 @@ class SectionFilterContent extends React.Component {
export default withRouter(
inject(({ auth, peopleStore }) => {
const { loadingStore, filterStore, usersStore, groupsStore } = peopleStore;
const {
loadingStore,
filterStore,
usersStore,
groupsStore,
viewAs,
} = peopleStore;
const { settingsStore, userStore, isLoaded, isAdmin } = auth;
const { customNames } = settingsStore;
const { user } = userStore;
@ -268,6 +276,7 @@ export default withRouter(
fetchPeople,
filter,
setIsLoading,
viewAs,
};
})(
observer(

View File

@ -12,19 +12,8 @@ import Headline from "@appserver/common/components/Headline";
import toastr from "studio/toastr";
import Loaders from "@appserver/common/components/Loaders";
import withLoader from "../../../../HOCs/withLoader";
import {
EmployeeType,
EmployeeStatus,
AppServerConfig,
} from "@appserver/common/constants";
import { AppServerConfig } from "@appserver/common/constants";
import { withTranslation } from "react-i18next";
import {
InviteDialog,
DeleteUsersDialog,
SendInviteDialog,
ChangeUserStatusDialog,
ChangeUserTypeDialog,
} from "../../../../components/dialogs";
import { isMobile } from "react-device-detect";
import { inject, observer } from "mobx-react";
import config from "../../../../../package.json";
@ -109,43 +98,24 @@ const StyledContainer = styled.div`
`;
const SectionHeaderContent = (props) => {
const [invitationDialogVisible, setInvitationDialogVisible] = useState(false);
const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
const [sendInviteDialogVisible, setSendInviteDialogVisible] = useState(false);
const [disableDialogVisible, setDisableDialogVisible] = useState(false);
const [activeDialogVisible, setActiveDialogVisible] = useState(false);
const [guestDialogVisible, setGuestDialogVisible] = useState(false);
const [employeeDialogVisible, setEmployeeDialogVisible] = useState(false);
const {
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
//onCheck,
//onSelect,
//clearSelection,
group,
isAdmin,
t,
tReady,
history,
customNames,
homepage,
deleteGroup,
selection,
hasAnybodySelected,
hasUsersToMakeEmployees,
hasUsersToMakeGuests,
hasUsersToActivate,
hasUsersToDisable,
hasUsersToInvite,
hasUsersToRemove,
isLoaded,
isTabletView,
//selectAll,
setSelected,
resetFilter,
//selectByStatus,
getHeaderMenu,
setInvitationDialogVisible,
viewAs,
} = props;
const {
@ -164,47 +134,7 @@ const SectionHeaderContent = (props) => {
setSelected,
]);
const onClose = () => {
setSelected("none");
};
const toggleEmployeeDialog = useCallback(
() => setEmployeeDialogVisible(!employeeDialogVisible),
[employeeDialogVisible]
);
const toggleGuestDialog = useCallback(
() => setGuestDialogVisible(!guestDialogVisible),
[guestDialogVisible]
);
const toggleActiveDialog = useCallback(
() => setActiveDialogVisible(!activeDialogVisible),
[activeDialogVisible]
);
const toggleDisableDialog = useCallback(
() => setDisableDialogVisible(!disableDialogVisible),
[disableDialogVisible]
);
const toggleSendInviteDialog = useCallback(
() => setSendInviteDialogVisible(!sendInviteDialogVisible),
[sendInviteDialogVisible]
);
const toggleDeleteDialog = useCallback(
() => setDeleteDialogVisible(!deleteDialogVisible),
[deleteDialogVisible]
);
const onSendEmail = useCallback(() => {
let str = "";
for (let item of selection) {
str += `${item.email},`;
}
window.open(`mailto: ${str}`, "_self");
}, [selection]);
const onClose = () => setSelected("none");
const onSelectorSelect = useCallback(
(item) => {
@ -213,91 +143,42 @@ const SectionHeaderContent = (props) => {
[onSelect]
);
const menuItems = useMemo(
let menuItems = useMemo(
() => [
{
label: t("Common:Select"),
isDropdown: true,
isSeparator: true,
isSelect: true,
fontWeight: "bold",
children: [
<DropDownItem
key="active"
label={t("Common:Active")}
data-index={0}
/>,
<DropDownItem
key="disabled"
label={t("Translations:DisabledEmployeeStatus")}
data-index={1}
/>,
<DropDownItem key="invited" label={t("LblInvited")} data-index={2} />,
],
onSelect: onSelectorSelect,
},
{
label: t("ChangeToUser", {
userCaption,
}),
disabled: !hasUsersToMakeEmployees,
onClick: toggleEmployeeDialog,
},
{
label: t("ChangeToGuest", {
guestCaption,
}),
disabled: !hasUsersToMakeGuests,
onClick: toggleGuestDialog,
},
{
label: t("LblSetActive"),
disabled: !hasUsersToActivate,
onClick: toggleActiveDialog,
},
{
label: t("LblSetDisabled"),
disabled: !hasUsersToDisable,
onClick: toggleDisableDialog,
},
{
label: t("LblInviteAgain"),
disabled: !hasUsersToInvite,
onClick: toggleSendInviteDialog,
},
{
label: t("LblSendEmail"),
disabled: !hasAnybodySelected,
onClick: onSendEmail,
},
{
label: t("Common:Delete"),
disabled: !hasUsersToRemove,
onClick: toggleDeleteDialog,
},
...[
{
label: t("Common:Select"),
isDropdown: true,
isSeparator: true,
isSelect: true,
fontWeight: "bold",
children: [
<DropDownItem
key="active"
label={t("Common:Active")}
data-index={0}
/>,
<DropDownItem
key="disabled"
label={t("Translations:DisabledEmployeeStatus")}
data-index={1}
/>,
<DropDownItem
key="invited"
label={t("LblInvited")}
data-index={2}
/>,
],
onSelect: onSelectorSelect,
},
],
],
[
t,
userCaption,
guestCaption,
onSelectorSelect,
toggleEmployeeDialog,
toggleGuestDialog,
toggleActiveDialog,
toggleDisableDialog,
toggleSendInviteDialog,
onSendEmail,
toggleDeleteDialog,
hasAnybodySelected,
hasUsersToMakeEmployees,
hasUsersToMakeGuests,
hasUsersToActivate,
hasUsersToDisable,
hasUsersToInvite,
hasUsersToRemove,
]
[t, onSelectorSelect]
);
const headerMenu = getHeaderMenu(t);
menuItems = [...menuItems, ...headerMenu];
const onEditGroup = useCallback(
() =>
history.push(
@ -349,10 +230,7 @@ const SectionHeaderContent = (props) => {
);
}, [history, homepage]);
const onInvitationDialogClick = useCallback(
() => setInvitationDialogVisible(!invitationDialogVisible),
[invitationDialogVisible]
);
const onInvitationDialogClick = () => setInvitationDialogVisible(true);
const getContextOptionsPlus = useCallback(() => {
return [
@ -391,7 +269,8 @@ const SectionHeaderContent = (props) => {
goToEmployeeCreate,
goToGuestCreate,
goToGroupCreate,
onInvitationDialogClick /* , onSentInviteAgain */,
/* , onSentInviteAgain */
,
]);
return (
@ -403,51 +282,7 @@ const SectionHeaderContent = (props) => {
width={context.sectionWidth}
isTabletView={isTabletView}
>
{employeeDialogVisible && (
<ChangeUserTypeDialog
visible={employeeDialogVisible}
onClose={toggleEmployeeDialog}
userType={EmployeeType.User}
/>
)}
{guestDialogVisible && (
<ChangeUserTypeDialog
visible={guestDialogVisible}
onClose={toggleGuestDialog}
userType={EmployeeType.Guest}
/>
)}
{activeDialogVisible && (
<ChangeUserStatusDialog
visible={activeDialogVisible}
onClose={toggleActiveDialog}
userStatus={EmployeeStatus.Active}
/>
)}
{disableDialogVisible && (
<ChangeUserStatusDialog
visible={disableDialogVisible}
onClose={toggleDisableDialog}
userStatus={EmployeeStatus.Disabled}
/>
)}
{sendInviteDialogVisible && (
<SendInviteDialog
visible={sendInviteDialogVisible}
onClose={toggleSendInviteDialog}
/>
)}
{deleteDialogVisible && (
<DeleteUsersDialog
visible={deleteDialogVisible}
onClose={toggleDeleteDialog}
/>
)}
{isHeaderVisible ? (
{isHeaderVisible && viewAs !== "table" ? (
<div className="group-button-menu-container">
<GroupButtonsMenu
checked={isHeaderChecked}
@ -508,13 +343,6 @@ const SectionHeaderContent = (props) => {
getData={getContextOptionsPlus}
isDisabled={false}
/>
{invitationDialogVisible && (
<InviteDialog
visible={invitationDialogVisible}
onClose={onInvitationDialogClick}
onCloseButton={onInvitationDialogClick}
/>
)}
</>
)}
</>
@ -528,34 +356,62 @@ const SectionHeaderContent = (props) => {
};
export default withRouter(
inject(({ auth, peopleStore }) => ({
resetFilter: peopleStore.resetFilter,
customNames: auth.settingsStore.customNames,
homepage: config.homepage,
isLoaded: auth.isLoaded,
isAdmin: auth.isAdmin,
fetchPeople: peopleStore.usersStore.getUsersList,
selection: peopleStore.selectionStore.selection,
setSelected: peopleStore.selectionStore.setSelected,
selectByStatus: peopleStore.selectionStore.selectByStatus,
isHeaderVisible: peopleStore.headerMenuStore.isHeaderVisible,
isHeaderIndeterminate: peopleStore.headerMenuStore.isHeaderIndeterminate,
isHeaderChecked: peopleStore.headerMenuStore.isHeaderChecked,
clearSelection: peopleStore.selectionStore.clearSelection,
selectAll: peopleStore.selectionStore.selectAll,
hasAnybodySelected: peopleStore.selectionStore.hasAnybodySelected,
hasUsersToMakeEmployees: peopleStore.selectionStore.hasUsersToMakeEmployees,
hasUsersToMakeGuests: peopleStore.selectionStore.hasUsersToMakeGuests,
hasUsersToActivate: peopleStore.selectionStore.hasUsersToActivate,
hasUsersToDisable: peopleStore.selectionStore.hasUsersToDisable,
hasUsersToInvite: peopleStore.selectionStore.hasUsersToInvite,
hasUsersToRemove: peopleStore.selectionStore.hasUsersToRemove,
deleteGroup: peopleStore.groupsStore.deleteGroup,
removeUser: peopleStore.usersStore.removeUser,
updateUserStatus: peopleStore.usersStore.updateUserStatus,
group: peopleStore.selectedGroupStore.group,
isTabletView: auth.settingsStore.isTabletView,
}))(
inject(({ auth, peopleStore }) => {
const { settingsStore, isLoaded, isAdmin } = auth;
const { customNames, isTabletView } = settingsStore;
const {
resetFilter,
usersStore,
selectionStore,
headerMenuStore,
groupsStore,
selectedGroupStore,
getHeaderMenu,
dialogStore,
viewAs,
} = peopleStore;
const { getUsersList, removeUser, updateUserStatus } = usersStore;
const {
setSelected,
selectByStatus,
clearSelection,
selectAll,
} = selectionStore;
const {
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
} = headerMenuStore;
const { deleteGroup } = groupsStore;
const { group } = selectedGroupStore;
const { setInvitationDialogVisible } = dialogStore;
return {
resetFilter,
customNames,
homepage: config.homepage,
isLoaded,
isAdmin,
fetchPeople: getUsersList,
setSelected,
selectByStatus,
isHeaderVisible,
isHeaderIndeterminate,
isHeaderChecked,
clearSelection,
selectAll,
deleteGroup,
removeUser,
updateUserStatus,
group,
isTabletView,
getHeaderMenu,
setInvitationDialogVisible,
viewAs,
};
})(
withTranslation(["Home", "Common", "Translations"])(
withLoader(observer(SectionHeaderContent))(<Loaders.SectionHeader />)
)

View File

@ -17,6 +17,7 @@ import {
} from "./Section";
import { inject, observer } from "mobx-react";
import { isMobile } from "react-device-detect";
import Dialogs from "./Section/Body/Dialogs"; //TODO: Move dialogs to another folder
const Home = ({
isLoading,
@ -56,39 +57,43 @@ const Home = ({
}, [isLoading]);
return (
<PageLayout
withBodyScroll
withBodyAutoFocus={!isMobile}
isLoading={isLoading}
>
<PageLayout.ArticleHeader>
<ArticleHeaderContent />
</PageLayout.ArticleHeader>
<>
<PageLayout
withBodyScroll
withBodyAutoFocus={!isMobile}
isLoading={isLoading}
>
<PageLayout.ArticleHeader>
<ArticleHeaderContent />
</PageLayout.ArticleHeader>
<PageLayout.ArticleMainButton>
<ArticleMainButtonContent />
</PageLayout.ArticleMainButton>
<PageLayout.ArticleMainButton>
<ArticleMainButtonContent />
</PageLayout.ArticleMainButton>
<PageLayout.ArticleBody>
<ArticleBodyContent />
</PageLayout.ArticleBody>
<PageLayout.ArticleBody>
<ArticleBodyContent />
</PageLayout.ArticleBody>
<PageLayout.SectionHeader>
<SectionHeaderContent />
</PageLayout.SectionHeader>
<PageLayout.SectionHeader>
<SectionHeaderContent />
</PageLayout.SectionHeader>
<PageLayout.SectionFilter>
<SectionFilterContent />
</PageLayout.SectionFilter>
<PageLayout.SectionFilter>
<SectionFilterContent />
</PageLayout.SectionFilter>
<PageLayout.SectionBody>
<SectionBodyContent />
</PageLayout.SectionBody>
<PageLayout.SectionBody>
<SectionBodyContent />
</PageLayout.SectionBody>
<PageLayout.SectionPaging>
<SectionPagingContent />
</PageLayout.SectionPaging>
</PageLayout>
<PageLayout.SectionPaging>
<SectionPagingContent />
</PageLayout.SectionPaging>
</PageLayout>
<Dialogs />
</>
);
};

View File

@ -745,7 +745,7 @@ class UpdateUserForm extends React.Component {
/>
{/*TODO: uncomment this after added phone form */}
{/* <TextChangeField
labelText={`${t("Phone")}:`}
labelText={`${t("Common:Phone")}:`}
inputName="phone"
inputValue={profile.mobilePhone}
buttonText={t("ChangeButton")}

View File

@ -7,6 +7,13 @@ class DialogStore {
deleteProfileEver = false;
data = {};
employeeDialogVisible = false;
guestDialogVisible = false;
activeDialogVisible = false;
disableDialogVisible = false;
sendInviteDialogVisible = false;
invitationDialogVisible = false;
constructor() {
makeAutoObservable(this);
}
@ -31,12 +38,48 @@ class DialogStore {
this.data = data;
};
setEmployeeDialogVisible = (visible) => {
this.employeeDialogVisible = visible;
};
setGuestDialogVisible = (visible) => {
this.guestDialogVisible = visible;
};
setActiveDialogVisible = (visible) => {
this.activeDialogVisible = visible;
};
setDisableDialogVisible = (visible) => {
this.disableDialogVisible = visible;
};
setSendInviteDialogVisible = (visible) => {
this.sendInviteDialogVisible = visible;
};
setDeleteDialogVisible = (visible) => {
this.deleteDialogVisible = visible;
};
setInvitationDialogVisible = (visible) => {
this.invitationDialogVisible = visible;
};
closeDialogs = () => {
this.setChangeEmailDialogVisible(false);
this.setChangePasswordDialogVisible(false);
this.setDeleteSelfProfileDialogVisible(false);
this.setDeleteProfileDialogVisible(false);
this.setDialogData({});
this.setEmployeeDialogVisible(false);
this.setGuestDialogVisible(false);
this.setActiveDialogVisible(false);
this.setDisableDialogVisible(false);
this.setSendInviteDialogVisible(false);
this.setDeleteDialogVisible(false);
this.setInvitationDialogVisible(false);
};
}

View File

@ -1,4 +1,4 @@
import { action, computed, makeObservable, observable } from "mobx";
import { makeAutoObservable } from "mobx";
import GroupsStore from "./GroupsStore";
import UsersStore from "./UsersStore";
import config from "../../package.json";
@ -29,6 +29,7 @@ class PeopleStore {
dialogStore = null;
loadingStore = null;
isInit = false;
viewAs = "table";
constructor() {
this.groupsStore = new GroupsStore(this);
@ -44,11 +45,7 @@ class PeopleStore {
this.dialogStore = new DialogStore();
this.loadingStore = new LoadingStore();
makeObservable(this, {
init: action,
isPeoplesAdmin: computed,
resetFilter: action,
});
makeAutoObservable(this);
}
get isPeoplesAdmin() {
@ -84,6 +81,82 @@ class PeopleStore {
return getUsersList(newFilter);
};
getHeaderMenu = (t) => {
const { userCaption, guestCaption } = authStore.settingsStore.customNames;
const {
hasUsersToMakeEmployees,
hasUsersToMakeGuests,
hasUsersToActivate,
hasUsersToDisable,
hasUsersToInvite,
hasAnybodySelected,
hasUsersToRemove,
selection,
} = this.selectionStore;
const {
setEmployeeDialogVisible,
setGuestDialogVisible,
setActiveDialogVisible,
setDisableDialogVisible,
setSendInviteDialogVisible,
setDeleteDialogVisible,
} = this.dialogStore;
const headerMenu = [
{
label: t("ChangeToUser", {
userCaption,
}),
disabled: !hasUsersToMakeEmployees,
onClick: () => setEmployeeDialogVisible(true),
},
{
label: t("ChangeToGuest", {
guestCaption,
}),
disabled: !hasUsersToMakeGuests,
onClick: () => setGuestDialogVisible(true),
},
{
label: t("LblSetActive"),
disabled: !hasUsersToActivate,
onClick: () => setActiveDialogVisible(true),
},
{
label: t("LblSetDisabled"),
disabled: !hasUsersToDisable,
onClick: () => setDisableDialogVisible(true),
},
{
label: t("LblInviteAgain"),
disabled: !hasUsersToInvite,
onClick: () => setSendInviteDialogVisible(true),
},
{
label: t("LblSendEmail"),
disabled: !hasAnybodySelected,
onClick: () => {
let str = "";
for (let item of selection) {
str += `${item.email},`;
}
window.open(`mailto: ${str}`, "_self");
},
},
{
label: t("Common:Delete"),
disabled: !hasUsersToRemove,
onClick: () => setDeleteDialogVisible(true),
},
];
return headerMenu;
};
setViewAs = (viewAs) => {
this.viewAs = viewAs;
};
}
export default PeopleStore;

View File

@ -44,8 +44,7 @@ class SelectionStore {
};
selectUser = (user) => {
const u = user;
return this.selection.push(u);
return this.selection.push(user);
};
deselectUser = (user) => {

View File

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.646447 2.14645C0.841709 1.95118 1.15829 1.95118 1.35355 2.14645L4 4.79289L6.64645 2.14645C6.84171 1.95118 7.15829 1.95118 7.35355 2.14645C7.54882 2.34171 7.54882 2.65829 7.35355 2.85355L4.35355 5.85355C4.15829 6.04882 3.84171 6.04882 3.64645 5.85355L0.646447 2.85355C0.451184 2.65829 0.451184 2.34171 0.646447 2.14645Z" fill="#657077"/>
</svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@ -81,5 +81,7 @@
"Version": "Version",
"View": "Anzeigen",
"ViewWeb": "Web-Version öffnen",
"Warning": "Warnung"
"Warning": "Warnung",
"Name": "Name",
"Phone": "Telefon"
}

View File

@ -95,5 +95,14 @@
"Warning": "Warning",
"ResetApplication": "Reset application",
"NewVersionAvailable": "A new version of the website available",
"Load": "Load"
"Load": "Load",
"Name": "Name",
"Image": "Image",
"Unknown": "Unknown",
"Archive": "Archive",
"Video": "Video",
"Audio": "Audio",
"Department": "Department",
"Role": "Role",
"Phone": "Phone"
}

View File

@ -71,5 +71,7 @@
"Version": "Version",
"View": "Afficher",
"ViewWeb": "Voir la version web",
"Warning": "Attention"
}
"Warning": "Attention",
"Name": "Nom",
"Phone": "Téléphone"
}

View File

@ -73,5 +73,7 @@
"Version": "Versione",
"View": "Visualizza",
"ViewWeb": "Visualizza la versione web",
"Warning": "Avviso"
"Warning": "Avviso",
"Name": "Nome",
"Phone": "Telefono"
}

View File

@ -73,5 +73,7 @@
"Version": "ລຸ້ນ",
"View": "ມຸມມອງ",
"ViewWeb": "ເປີດຜ່ານຫນ້າເວັບ",
"Warning": "ແຈ້ງເຕືອນ"
"Warning": "ແຈ້ງເຕືອນ",
"Name": "ຊື່",
"Phone": "ໂທລະສັບ"
}

View File

@ -73,5 +73,7 @@
"Version": "Versão",
"View": "Ver",
"ViewWeb": "Veja a versão web",
"Warning": "Aviso"
"Warning": "Aviso",
"Name": "Nome",
"Phone": "Telefone"
}

View File

@ -73,5 +73,7 @@
"Version": "Versiune",
"View": "Vizualizare",
"ViewWeb": "Accesați versiunea online",
"Warning": "Avertisment"
"Warning": "Avertisment",
"Name": "Numele",
"Phone": "Telefon"
}

View File

@ -95,5 +95,14 @@
"Warning": "Внимание",
"ResetApplication": "Сбросить приложение",
"NewVersionAvailable": "Доступна новая версия веб-сайта",
"Load": "Загрузить"
"Load": "Загрузить",
"Name": "Название",
"Image": "Изображение",
"Unknown": "Неизвестный",
"Archive": "Архив",
"Video": "Видео",
"Audio": "Аудио",
"Department": "Отдел",
"Role": "Позиция",
"Phone": "Телефон"
}