Web:Client:OAuth: fix mobx warning, add infinity list for table view

This commit is contained in:
Timofey Boyko 2023-09-27 18:13:07 +03:00
parent 2381fcf68f
commit 807215a0cb
10 changed files with 423 additions and 212 deletions

View File

@ -8,5 +8,5 @@ export interface OAuthProps {
viewAs: ViewAsType;
clientList: ClientProps[];
isEmptyClientList: boolean;
fetchClients: (page: number) => Promise<void>;
fetchClients: () => Promise<void>;
}

View File

@ -24,8 +24,8 @@ const OAuth = ({
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const startLoadingRef = React.useRef<null | Date>(null);
const getClientList = React.useCallback(async (page = 0) => {
await fetchClients(page);
const getClientList = React.useCallback(async () => {
await fetchClients();
if (startLoadingRef.current) {
const currentDate = new Date();

View File

@ -1,18 +1,9 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
//@ts-ignore
import TableHeader from "@docspace/components/table-container/TableHeader";
const TABLE_VERSION = "1";
const TABLE_COLUMNS = `oauthConfigColumns_ver-${TABLE_VERSION}`;
interface HeaderProps {
sectionWidth: number;
tableRef: HTMLDivElement;
columnStorageName: string;
}
import { HeaderProps } from "./TableView.types";
const Header = (props: HeaderProps) => {
const { sectionWidth, tableRef, columnStorageName } = props;

View File

@ -1,118 +1,31 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
//@ts-ignore
import TableRow from "@docspace/components/table-container/TableRow";
//@ts-ignore
import TableCell from "@docspace/components/table-container/TableCell";
import Text from "@docspace/components/text";
import ToggleButton from "@docspace/components/toggle-button";
import { Base } from "@docspace/components/themes";
import SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
import { ClientProps } from "@docspace/common/utils/oauth/interfaces";
import NameCell from "./columns/name";
const StyledWrapper = styled.div`
display: contents;
`;
const StyledTableRow = styled(TableRow)`
.table-container_cell {
text-overflow: ellipsis;
padding-right: 8px;
}
.mr-8 {
margin-right: 8px;
}
.textOverflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toggleButton {
display: contents;
input {
position: relative;
margin-left: -8px;
}
}
.table-container_row-loader {
margin-left: 8px;
margin-right: 16px;
}
:hover {
.table-container_cell {
cursor: pointer;
background: ${(props) =>
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
margin-top: -1px;
border-top: ${(props) =>
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
}
.table-container_file-name-cell {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: -24px;
padding-right: 24px;
`
: css`
margin-left: -24px;
padding-left: 24px;
`}
}
.table-container_row-context-menu-wrapper {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: -20px;
padding-left: 18px;
`
: css`
margin-right: -20px;
padding-right: 18px;
`}
}
}
`;
StyledTableRow.defaultProps = { theme: Base };
interface RowProps {
item: ClientProps;
isChecked: boolean;
inProgress: boolean;
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
import { StyledRowWrapper, StyledTableRow } from "./TableView.styled";
import { RowProps } from "./TableView.types";
const Row = (props: RowProps) => {
const { item, changeClientStatus, isChecked, inProgress, setSelection } =
props;
const {
item,
changeClientStatus,
isChecked,
inProgress,
getContextMenuItems,
setSelection,
} = props;
const navigate = useNavigate();
const { t } = useTranslation(["Webhooks", "Common"]);
const editClient = () => {
navigate(window.location.pathname + `/${item.clientId}`);
navigate(`${item.clientId}`);
};
const handleToggleEnabled = async () => {
@ -120,45 +33,31 @@ const Row = (props: RowProps) => {
await changeClientStatus(item.clientId, !item.enabled);
};
const onDeleteOpen = () => {};
const handleRowClick = (e: any) => {
if (
e.target.closest(".checkbox") ||
e.target.closest(".table-container_row-checkbox") ||
e.target.closest(".type-combobox") ||
e.target.closest(".table-container_row-context-menu-wrapper") ||
e.target.closest(".toggleButton") ||
e.detail === 0
) {
return;
}
if (
e.target.closest(".type-combobox") ||
e.target.closest(".table-container_row-context-menu-wrapper") ||
e.target.closest(".toggleButton")
) {
return setSelection && setSelection("");
}
editClient();
};
const contextOptions = [
{
key: "settings",
label: t("Common:Settings"),
icon: SettingsIcon,
onClick: editClient,
},
{
key: "Separator dropdownItem",
isSeparator: true,
},
{
key: "delete",
label: t("Common:Delete"),
icon: DeleteIcon,
onClick: onDeleteOpen,
},
];
const contextOptions = getContextMenuItems && getContextMenuItems(t, item);
return (
<>
<StyledWrapper className="handle">
<StyledRowWrapper className="handle">
<StyledTableRow
contextOptions={contextOptions}
onClick={handleRowClick}
@ -187,7 +86,7 @@ const Row = (props: RowProps) => {
/>
</TableCell>
</StyledTableRow>
</StyledWrapper>
</StyledRowWrapper>
</>
);
};

View File

@ -0,0 +1,98 @@
import styled, { css } from "styled-components";
//@ts-ignore
import TableContainer from "@docspace/components/table-container/TableContainer";
//@ts-ignore
import TableRow from "@docspace/components/table-container/TableRow";
import { Base } from "@docspace/components/themes";
export const TableWrapper = styled(TableContainer)`
margin-top: 0px;
.header-container-text {
font-size: 12px;
}
.table-container_header {
position: absolute;
}
`;
const StyledRowWrapper = styled.div`
display: contents;
`;
const StyledTableRow = styled(TableRow)`
.table-container_cell {
text-overflow: ellipsis;
padding-right: 8px;
}
.mr-8 {
margin-right: 8px;
}
.textOverflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toggleButton {
display: contents;
input {
position: relative;
margin-left: -8px;
}
}
.table-container_row-loader {
margin-left: 8px;
margin-right: 16px;
}
:hover {
.table-container_cell {
cursor: pointer;
background: ${(props) =>
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
margin-top: -1px;
border-top: ${(props) =>
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
}
.table-container_file-name-cell {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: -24px;
padding-right: 24px;
`
: css`
margin-left: -24px;
padding-left: 24px;
`}
}
.table-container_row-context-menu-wrapper {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: -20px;
padding-left: 18px;
`
: css`
margin-right: -20px;
padding-right: 18px;
`}
}
}
`;
StyledTableRow.defaultProps = { theme: Base };
export { StyledRowWrapper, StyledTableRow };

View File

@ -0,0 +1,46 @@
import { ClientProps } from "@docspace/common/utils/oauth/interfaces";
//@ts-ignore
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
export interface TableViewProps {
items: ClientProps[];
sectionWidth: number;
viewAs?: ViewAsType;
setViewAs?: (value: ViewAsType) => void;
userId?: string;
selection?: string[];
setSelection?: (clientId: string) => void;
getContextMenuItems?: (
t: any,
item: ClientProps
) => {
[key: string]: any | string | boolean | ((clientId: string) => void);
}[];
bufferSelection?: ClientProps | null;
activeClients?: string[];
hasNextPage?: boolean;
totalElements?: number;
fetchNextClients?: (startIndex: number) => Promise<void>;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
export interface HeaderProps {
sectionWidth: number;
tableRef: HTMLDivElement;
columnStorageName: string;
}
export interface RowProps {
item: ClientProps;
isChecked: boolean;
inProgress: boolean;
getContextMenuItems?: (
t: any,
item: ClientProps
) => {
[key: string]: any | string | boolean | ((clientId: string) => void);
}[];
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}

View File

@ -24,8 +24,8 @@ const StyledImage = styled.img`
interface NameCellProps {
name: string;
icon?: string;
clientId: string;
icon?: string;
inProgress?: boolean;
isChecked?: boolean;
setSelection?: (clientId: string) => void;
@ -39,7 +39,7 @@ const NameCell = ({
isChecked,
setSelection,
}: NameCellProps) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onChange = () => {
setSelection && setSelection(clientId);
};

View File

@ -1,66 +1,35 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { isMobile } from "react-device-detect";
import styled from "styled-components";
import { ClientProps } from "@docspace/common/utils/oauth/interfaces";
//@ts-ignore
import TableContainer from "@docspace/components/table-container/TableContainer";
//@ts-ignore
import TableBody from "@docspace/components/table-container/TableBody";
//@ts-ignore
import { OAuthStoreProps, ViewAsType } from "SRC_DIR/store/OAuthStore";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import Row from "./Row";
import Header from "./Header";
const TableWrapper = styled(TableContainer)`
margin-top: 0px;
.header-container-text {
font-size: 12px;
}
.table-container_header {
position: absolute;
}
`;
import { TableViewProps } from "./TableView.types";
import { TableWrapper } from "./TableView.styled";
const TABLE_VERSION = "1";
const COLUMNS_SIZE = `oauthConfigColumnsSize_ver-${TABLE_VERSION}`;
interface TableViewProps {
items: ClientProps[];
sectionWidth: number;
viewAs?: ViewAsType;
setViewAs?: (value: ViewAsType) => void;
userId?: string;
selection?: string[];
setSelection?: (clientId: string) => void;
bufferSelection?: ClientProps | null;
currentClient?: ClientProps | null;
setBufferSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
const TableView = ({
items,
sectionWidth,
viewAs,
setViewAs,
selection,
bufferSelection,
activeClients,
setSelection,
getContextMenuItems,
changeClientStatus,
userId,
hasNextPage,
totalElements,
fetchNextClients,
}: TableViewProps) => {
const tableRef = React.useRef<HTMLDivElement>(null);
@ -73,6 +42,29 @@ const TableView = ({
}
}, [sectionWidth, viewAs, setViewAs]);
const clickOutside = React.useCallback(
(e: any) => {
if (
e.target.closest(".checkbox") ||
e.target.closest(".table-container_row-checkbox") ||
e.detail === 0
) {
return;
}
setSelection && setSelection("");
},
[setSelection]
);
React.useEffect(() => {
window.addEventListener("click", clickOutside);
return () => {
window.removeEventListener("click", clickOutside);
};
}, [clickOutside, setSelection]);
const columnStorageName = `${COLUMNS_SIZE}=${userId}`;
return (
@ -88,18 +80,24 @@ const TableView = ({
useReactWindow
columnStorageName={columnStorageName}
filesLength={items.length}
fetchMoreFiles={() => console.log("call")}
hasMoreFiles={false}
itemCount={items.length}
fetchMoreFiles={({
startIndex,
}: {
startIndex: number;
stopIndex: number;
}) => fetchNextClients && fetchNextClients(startIndex)}
hasMoreFiles={hasNextPage}
itemCount={totalElements}
>
{items.map((item, index) => (
{items.map((item) => (
<Row
key={item.clientId}
item={item}
isChecked={selection?.includes(item.clientId) || false}
inProgress={false}
inProgress={activeClients?.includes(item.clientId) || false}
setSelection={setSelection}
changeClientStatus={changeClientStatus}
getContextMenuItems={getContextMenuItems}
/>
))}
</TableBody>
@ -115,26 +113,29 @@ export default inject(
viewAs,
setViewAs,
selection,
bufferSelection,
setSelection,
setBufferSelection,
changeClientStatus,
currentClient,
getContextMenuItems,
activeClients,
hasNextPage,
totalElements,
fetchNextClients,
} = oauthStore;
return {
viewAs,
setViewAs,
userId,
changeClientStatus,
selection,
bufferSelection,
setSelection,
setBufferSelection,
currentClient,
activeClients,
getContextMenuItems,
hasNextPage,
totalElements,
fetchNextClients,
};
}
)(observer(TableView));

View File

@ -1,4 +1,4 @@
import { makeAutoObservable } from "mobx";
import { makeAutoObservable, runInAction } from "mobx";
//@ts-ignore
import { getPortal } from "@docspace/common/api/portal";
@ -21,19 +21,37 @@ import {
Scope,
} from "@docspace/common/utils/oauth/interfaces";
import SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
import EnableReactSvgUrl from "PUBLIC_DIR/images/enable.react.svg?url";
import RemoveReactSvgUrl from "PUBLIC_DIR/images/remove.react.svg?url";
const PAGE_LIMIT = 20;
export type ViewAsType = "table" | "row";
export interface OAuthStoreProps {
viewAs: ViewAsType;
setViewAs: (value: "table" | "row") => void;
deleteDialogVisible: boolean;
setDeleteDialogVisible: (value: boolean) => void;
editClient: (clientId: string) => void;
clients: ClientProps[];
fetchClient: (clientId: string) => Promise<ClientProps | undefined>;
fetchClients: () => Promise<void>;
fetchNextClients: (startIndex: number) => Promise<void>;
saveClient: (client: ClientProps) => Promise<void>;
updateClient: (clientId: string, client: ClientProps) => Promise<void>;
changeClientStatus: (clientId: string, status: boolean) => Promise<void>;
regenerateSecret: (clientId: string) => Promise<string | undefined>;
deleteClient: (clientId: string) => Promise<void>;
currentPage: number;
totalPages: number;
totalElements: number;
selection: string[];
setSelection: (clientId: string) => void;
@ -44,21 +62,20 @@ export interface OAuthStoreProps {
tenant: number;
fetchTenant: () => Promise<number>;
activeClients: string[];
setActiveClient: (clientId: string) => void;
scopes: Scope[];
setViewAs: (value: "table" | "row") => void;
fetchClient: (clientId: string) => Promise<ClientProps | undefined>;
fetchClients: (page: number) => Promise<void>;
saveClient: (client: ClientProps) => Promise<void>;
updateClient: (clientId: string, client: ClientProps) => Promise<void>;
changeClientStatus: (clientId: string, status: boolean) => Promise<void>;
regenerateSecret: (clientId: string) => Promise<string | undefined>;
deleteClient: (clientId: string) => Promise<void>;
fetchScope: (name: string) => Promise<Scope | undefined>;
fetchScopes: () => Promise<void>;
getContextMenuItems: (
t: any,
item: ClientProps
) => {
[key: string]: any | string | boolean | ((clientId: string) => void);
}[];
clientList: ClientProps[];
isEmptyClientList: boolean;
hasNextPage: boolean;
@ -70,6 +87,7 @@ class OAuthStore implements OAuthStoreProps {
currentPage: number = 0;
totalPages: number = 0;
totalElements: number = 0;
deleteDialogVisible: boolean = false;
@ -81,6 +99,8 @@ class OAuthStore implements OAuthStoreProps {
clients: ClientProps[] = [];
activeClients: string[] = [];
scopes: Scope[] = [];
constructor() {
@ -96,10 +116,14 @@ class OAuthStore implements OAuthStoreProps {
};
setSelection = (clientId: string) => {
if (this.selection.includes(clientId)) {
this.selection = this.selection.filter((s) => s !== clientId);
if (!clientId) {
this.selection = [];
} else {
this.selection.push(clientId);
if (this.selection.includes(clientId)) {
this.selection = this.selection.filter((s) => s !== clientId);
} else {
this.selection.push(clientId);
}
}
};
@ -111,6 +135,25 @@ class OAuthStore implements OAuthStoreProps {
}
};
setActiveClient = (clientId: string) => {
if (!clientId) {
this.activeClients = [];
} else {
if (this.activeClients.includes(clientId)) {
this.activeClients = this.activeClients.filter((s) => s !== clientId);
} else {
this.activeClients.push(clientId);
}
}
};
editClient = (clientId: string) => {
//@ts-ignore
window?.DocSpace?.navigate(
`/portal-settings/developer-tools/oauth/${clientId}`
);
};
fetchTenant = async () => {
if (this.tenant > -1) return this.tenant;
@ -130,27 +173,52 @@ class OAuthStore implements OAuthStoreProps {
}
};
fetchClients = async (page: number) => {
fetchClients = async () => {
try {
runInAction(() => {
this.currentPage = 1;
});
const clientList: ClientListProps = await getClientList(0, PAGE_LIMIT);
this.totalPages = clientList.totalPages;
this.currentPage = page;
this.clients = clientList.content;
runInAction(() => {
this.totalPages = clientList.totalPages;
this.totalElements = clientList.totalElements;
this.clients = clientList.content;
});
} catch (e) {
console.log(e);
}
};
fetchNextClients = async (startIndex: number) => {
const page = startIndex / PAGE_LIMIT;
runInAction(() => {
this.currentPage = page + 1;
});
const clientList: ClientListProps = await getClientList(page, PAGE_LIMIT);
runInAction(() => {
this.totalPages = clientList.totalPages;
this.totalElements = clientList.totalElements;
this.clients = [...this.clients, ...clientList.content];
});
};
//TODO: OAuth, add tenant and other params
saveClient = async (client: ClientProps) => {
try {
client.tenant = 1;
client.authenticationMethod = "zxc";
client.termsUrl = "zxc";
const newClient = await addClient(client);
this.clients.push(newClient);
runInAction(() => {
this.clients.push(newClient);
});
} catch (e) {
console.log(e);
}
@ -163,7 +231,9 @@ class OAuthStore implements OAuthStoreProps {
const idx = this.clients.findIndex((c) => c.clientId === clientId);
if (idx > -1) {
this.clients[idx] = newClient;
runInAction(() => {
this.clients[idx] = newClient;
});
}
} catch (e) {
console.log(e);
@ -177,7 +247,9 @@ class OAuthStore implements OAuthStoreProps {
const idx = this.clients.findIndex((c) => c.clientId === clientId);
if (idx > -1) {
this.clients[idx] = { ...this.clients[idx], enabled: status };
runInAction(() => {
this.clients[idx] = { ...this.clients[idx], enabled: status };
});
}
} catch (e) {
console.log(e);
@ -222,6 +294,109 @@ class OAuthStore implements OAuthStoreProps {
}
};
getContextMenuItems = (t: any, item: ClientProps) => {
const { clientId } = item;
const isGroupContext = this.selection.length;
const onDelete = () => {
if (!isGroupContext) {
this.setBufferSelection(clientId);
}
this.setDeleteDialogVisible(true);
};
const onEnable = async (status: boolean) => {
if (isGroupContext) {
try {
const actions: Promise<void>[] = [];
this.selection.forEach((s) => {
this.setActiveClient(s);
actions.push(this.changeClientStatus(s, status));
});
await Promise.all(actions);
runInAction(() => {
this.activeClients = [];
this.selection = [];
});
//TODO OAuth, show toast
} catch (e) {}
} else {
this.setActiveClient(clientId);
await this.changeClientStatus(clientId, status);
//TODO OAuth, show toast
}
};
const settingsOption = {
key: "settings",
icon: SettingsIcon,
label: t("Common:Settings"),
onClick: () => this.editClient(clientId),
};
const enableOption = {
key: "enable",
icon: EnableReactSvgUrl,
label: t("Common:Enable"),
onClick: () => onEnable(true),
};
const disableOption = {
key: "disable",
icon: RemoveReactSvgUrl,
label: t("Common:Disable"),
onClick: () => onEnable(false),
};
const contextOptions = [
{
key: "Separator dropdownItem",
isSeparator: true,
},
{
key: "delete",
label: t("Common:Delete"),
icon: DeleteIcon,
onClick: () => onDelete(),
},
];
if (isGroupContext) {
let enabled = false;
this.selection.forEach((s) => {
enabled =
enabled ||
this.clientList.find((client) => client.clientId === s)?.enabled ||
false;
});
if (enabled) {
contextOptions.unshift(disableOption);
} else {
contextOptions.unshift(enableOption);
}
} else {
if (item.enabled) {
contextOptions.unshift(disableOption);
} else {
contextOptions.unshift(enableOption);
}
contextOptions.unshift(settingsOption);
}
return contextOptions;
};
get clientList() {
return this.clients;
}

View File

@ -92,6 +92,7 @@
"Delete": "Delete",
"DescriptionOfTheEveryoneRole": "The form is available for filling out by all participants of this room.",
"DescriptionOfTheRoleQueue": "Each form filled out by users from the first role will go in turn to the next users listed below.",
"Disable": "Disable",
"Disconnect": "Disconnect",
"DocSpaceAdmin": "DocSpace admin",
"DocSpaceOwner": "DocSpace owner",