Client:Pages:OAuth2: fix create and edit form after merge develop

This commit is contained in:
Timofey Boyko 2024-05-27 10:21:18 +03:00
parent 33bc642d0d
commit e7c97f0806
32 changed files with 674 additions and 598 deletions

View File

@ -49,7 +49,7 @@ import {
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import TariffBar from "SRC_DIR/components/TariffBar";
const HeaderContainer = styled.div`
export const HeaderContainer = styled.div`
position: relative;
display: flex;
align-items: center;
@ -155,7 +155,7 @@ const HeaderContainer = styled.div`
}
`;
const StyledContainer = styled.div`
export const StyledContainer = styled.div`
.group-button-menu-container {
${(props) =>
props.viewAs === "table"

View File

@ -89,6 +89,8 @@ const Layout = ({
const webhookHistoryPath = `/portal-settings/developer-tools/webhooks/${id}`;
const webhookDetailsPath = `/portal-settings/developer-tools/webhooks/${id}/${eventId}`;
const oauthCreatePath = `/portal-settings/developer-tools/oauth/create`;
const oauthEditPath = `/portal-settings/developer-tools/oauth/${id}`;
const currentPath = window.location.pathname;
return (
@ -108,6 +110,9 @@ const Layout = ({
<HistoryHeader />
) : currentPath === webhookDetailsPath ? (
<DetailsNavigationHeader />
) : currentPath === oauthCreatePath ||
currentPath === oauthEditPath ? (
<OAuthSectionHeader />
) : (
<SectionHeaderContent />
)}

View File

@ -1,8 +1,7 @@
//@ts-ignore
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import { IClientProps } from "@docspace/shared/utils/oauth/interfaces";
//@ts-ignore
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
export interface OAuthProps {

View File

@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
//@ts-ignore
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
import ClientForm from "../sub-components/ClientForm";
@ -10,7 +10,7 @@ const OAuthCreatePage = () => {
React.useEffect(() => {
setDocumentTitle(t("OAuth"));
}, []);
}, [t]);
return <ClientForm />;
};

View File

@ -2,7 +2,6 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
//@ts-ignore
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
import ClientForm from "../sub-components/ClientForm";
@ -14,7 +13,7 @@ const OAuthEditPage = () => {
React.useEffect(() => {
setDocumentTitle(t("OAuth"));
}, []);
}, [t]);
return <ClientForm id={id} />;
};

View File

@ -1,63 +0,0 @@
import styled, { css } from "styled-components";
import { isMobile, isMobileOnly } from "react-device-detect";
import { Base } from "@docspace/shared/themes";
import { tablet } from "@docspace/shared/utils/device";
const HeaderContainer = styled.div`
position: sticky;
top: 0;
background-color: ${(props) => props.theme.backgroundColor};
z-index: 201;
display: flex;
align-items: center;
width: 100%;
min-height: 70px;
flex-wrap: wrap;
${() =>
isMobile &&
css`
margin-bottom: 11px;
`}
${() =>
isMobileOnly &&
css`
margin-top: 7px;
margin-left: -14px;
padding-left: 14px;
margin-right: -14px;
padding-right: 14px;
`}
.arrow-button {
margin-inline-end: 18.5px;
@media ${tablet} {
padding-block: 8px;
padding-inline: 8px 0;
margin-inline-start: -8px;
}
${() =>
isMobileOnly &&
css`
margin-inline-end: 13px;
`}
svg {
${({ theme }) =>
theme.interfaceDirection === "rtl" && "transform: scaleX(-1);"}
}
}
.headline {
font-size: 18px;
margin-inline-end: 16px;
}
`;
HeaderContainer.defaultProps = { theme: Base };
export { HeaderContainer };

View File

@ -5,12 +5,15 @@ import Headline from "@docspace/shared/components/headline/Headline";
import { IconButton } from "@docspace/shared/components/icon-button";
import ArrowPathReactSvgUrl from "PUBLIC_DIR/images/arrow.path.react.svg?url";
import LoaderSectionHeader from "SRC_DIR/pages/PortalSettings/Layout/Section/loaderSectionHeader";
import { HeaderContainer } from "./SectionHeader.styled";
import { OAuthSectionHeaderProps } from "./SectionHeader.types";
import {
StyledContainer,
HeaderContainer,
} from "../../../../Layout/Section/Header";
const OAuthSectionHeader = ({ isEdit }: OAuthSectionHeaderProps) => {
const { t } = useTranslation(["OAuth"]);
const OAuthSectionHeader = ({ isEdit }: { isEdit: boolean }) => {
const { t, ready } = useTranslation(["OAuth"]);
const navigate = useNavigate();
@ -18,25 +21,28 @@ const OAuthSectionHeader = ({ isEdit }: OAuthSectionHeaderProps) => {
navigate("/portal-settings/developer-tools/oauth");
};
const NavigationHeader = () => (
<>
<IconButton
iconName={ArrowPathReactSvgUrl}
size={17}
isFill
onClick={onBack}
className="arrow-button"
/>
<Headline type="content" truncate className="headline">
{isEdit ? t("EditApp") : t("NewApp")}
</Headline>
</>
);
if (!ready) return <LoaderSectionHeader />;
return (
<HeaderContainer>
<NavigationHeader />
</HeaderContainer>
<StyledContainer>
<HeaderContainer>
<Headline type="content" truncate>
<div className="settings-section_header">
<div className="header">
<IconButton
iconName={ArrowPathReactSvgUrl}
size={17}
isFill
onClick={onBack}
className="arrow-button"
/>
{isEdit ? t("EditApp") : t("NewApp")}
</div>
</div>
</Headline>
</HeaderContainer>
</StyledContainer>
);
};

View File

@ -2,4 +2,9 @@ import styled from "styled-components";
export const OAuthContainer = styled.div`
width: 100%;
.ec-subheading {
margin-top: 8px;
text-align: center;
}
`;

View File

@ -1,12 +1,11 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
//@ts-ignore
import useViewEffect from "SRC_DIR/Hooks/useViewEffect";
//@ts-ignore
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import useViewEffect from "SRC_DIR/Hooks/useViewEffect";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
//@ts-ignore
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
import OAuthEmptyScreen from "./sub-components/EmptyScreen";
@ -19,7 +18,6 @@ import PreviewDialog from "./sub-components/PreviewDialog";
import OAuthLoader from "./sub-components/List/Loader";
import DisableDialog from "./sub-components/DisableDialog";
import DeleteDialog from "./sub-components/DeleteDialog";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
const MIN_LOADER_TIME = 500;
@ -55,7 +53,7 @@ const OAuth = ({
const currentDate = new Date();
const ms = Math.abs(
startLoadingRef.current.getTime() - currentDate.getTime()
startLoadingRef.current.getTime() - currentDate.getTime(),
);
if (ms < MIN_LOADER_TIME)
@ -147,5 +145,5 @@ export default inject(
disableDialogVisible,
deleteDialogVisible,
};
}
},
)(observer(OAuth));

View File

@ -89,6 +89,10 @@ const StyledInputGroup = styled.div`
gap: 0px;
}
.label {
height: 20px;
}
.select {
display: flex;
flex-direction: row;
@ -130,6 +134,8 @@ StyledInputGroup.defaultProps = { theme: Base };
const StyledInputRow = styled.div`
width: 100%;
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
@ -224,6 +230,63 @@ const StyledButtonContainer = styled.div`
}
`;
const StyledInputAddBlock = styled.div`
width: calc(100% - 40px);
height: 44px;
padding: 0 6px;
box-sizing: border-box;
cursor: pointer;
z-index: 200;
display: none;
align-items: center;
justify-content: space-between;
gap: 10px;
background: ${(props) => props.theme.backgroundColor};
position: absolute;
top: 40px;
left: 0px;
border-radius: 3px;
border: 1px solid #d0d5da;
box-shadow: ${(props) => props.theme.navigation.boxShadow};
.add-block {
display: flex;
align-items: center;
gap: 4px;
p {
color: #4781d1;
}
svg path {
fill: #4781d1;
}
}
`;
const StyledCheckboxGroup = styled.div`
width: 100%;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-top: 4px;
`;
export {
StyledContainer,
StyledBlock,
@ -236,4 +299,6 @@ export {
StyledScopesName,
StyledScopesCheckbox,
StyledButtonContainer,
StyledInputAddBlock,
StyledCheckboxGroup,
};

View File

@ -1,14 +1,13 @@
//@ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import {
IClientProps,
IClientReqDTO,
IScope,
} from "@docspace/shared/utils/oauth/interfaces";
//@ts-ignore
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
export interface InputProps {
value: string;
name: string;
@ -44,14 +43,10 @@ export interface ClientFormProps {
scopeList?: IScope[];
fetchClient?: (clientId: string) => Promise<IClientProps>;
fetchScopes?: () => Promise<void>;
saveClient?: (client: IClientReqDTO) => Promise<IClientProps>;
updateClient?: (
clientId: string,
client: IClientReqDTO
) => Promise<IClientReqDTO>;
saveClient?: (client: IClientReqDTO) => Promise<void>;
updateClient?: (clientId: string, client: IClientReqDTO) => Promise<void>;
resetDialogVisible?: boolean;
setResetDialogVisible?: (value: boolean) => void;

View File

@ -0,0 +1,9 @@
export function isValidUrl(url: string) {
try {
const newUrl = new URL(url);
if (newUrl) return true;
return false;
} catch (err) {
return false;
}
}

View File

@ -1,12 +1,13 @@
import React from "react";
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
//@ts-ignore
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
import {
StyledBlock,
StyledButtonContainer,
StyledCheckboxGroup,
StyledContainer,
StyledHeaderRow,
StyledInputBlock,
@ -18,13 +19,11 @@ import {
} from "./ClientForm.styled";
const HelpButtonSkeleton = () => {
return <RectangleSkeleton width={"12px"} height={"12px"} />;
return <RectangleSkeleton width="12px" height="12px" />;
};
const CheckboxSkeleton = ({ className }: { className?: string }) => {
return (
<RectangleSkeleton className={className} width={"16px"} height={"16px"} />
);
return <RectangleSkeleton className={className} width="16px" height="16px" />;
};
const ClientFormLoader = ({
@ -40,72 +39,82 @@ const ClientFormLoader = ({
<StyledContainer>
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width={"78px"} height={"16px"} />
<RectangleSkeleton width="78px" height="22px" />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"65px"} height={"13px"} />
<RectangleSkeleton width="65px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width={"100%"} height={"32px"} />
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"80px"} height={"13px"} />
<RectangleSkeleton width="80px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width={"100%"} height={"32px"} />
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<div className="label">
<RectangleSkeleton width={"60px"} height={"13px"} />
<RectangleSkeleton width="60px" height="20px" />
</div>
<div className="select">
<RectangleSkeleton width={"32px"} height={"32px"} />
<RectangleSkeleton width={"32px"} height={"32px"} />
<RectangleSkeleton width={"109px"} height={"13px"} />
<RectangleSkeleton width="32px" height="32px" />
<RectangleSkeleton width="32px" height="32px" />
<RectangleSkeleton width="109px" height="20px" />
</div>
<RectangleSkeleton width={"130px"} height={"12px"} />
<RectangleSkeleton width="130px" height="16px" />
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"75px"} height={"13px"} />
<RectangleSkeleton width="75px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width={"100%"} height={"60px"} />
<RectangleSkeleton width="100%" height="60px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width="75px" height="20px" />
</StyledHeaderRow>
<StyledCheckboxGroup>
<CheckboxSkeleton />
<RectangleSkeleton width="151px" height="18px" />
<HelpButtonSkeleton />
</StyledCheckboxGroup>
</StyledInputGroup>
</StyledInputBlock>
</StyledBlock>
{isEdit && (
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width={"47px"} height={"16px"} />
<RectangleSkeleton width="47px" height="22px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"96px"} height={"13px"} />
<RectangleSkeleton width="96px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width={"100%"} height={"32px"} />
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"60px"} height={"13px"} />
<RectangleSkeleton width="60px" height="20px" />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton
className={"loader"}
width={"calc(100% - 91px)"}
height={"32px"}
className="loader"
width="calc(100% - 91px)"
height="32px"
/>
<RectangleSkeleton width={"91px"} height={"32px"} />
<RectangleSkeleton width="91px" height="32px" />
</StyledInputRow>
</StyledInputGroup>
</StyledInputBlock>
@ -113,174 +122,179 @@ const ClientFormLoader = ({
)}
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width={"96px"} height={"16px"} />
<RectangleSkeleton width="96px" height="22px" />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"87px"} height={"13px"} />
<RectangleSkeleton width="87px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton
className={"loader"}
width={"calc(100% - 40px)"}
height={"32px"}
className="loader"
width="calc(100% - 40px)"
height="32px"
/>
<RectangleSkeleton width={"32px"} height={"32px"} />
<RectangleSkeleton width="32px" height="32px" />
</StyledInputRow>
<RectangleSkeleton width={"162px"} height={"32px"} />
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"96px"} height={"13px"} />
<RectangleSkeleton width="96px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton
className={"loader"}
width={"calc(100% - 40px)"}
height={"32px"}
className="loader"
width="calc(100% - 40px)"
height="32px"
/>
<RectangleSkeleton width={"32px"} height={"32px"} />
<RectangleSkeleton width="32px" height="32px" />
</StyledInputRow>
<RectangleSkeleton width={"162px"} height={"32px"} />
</StyledInputGroup>
</StyledInputBlock>
</StyledBlock>
<StyledScopesContainer>
<StyledHeaderRow className="header">
<RectangleSkeleton width={"111px"} height={"16px"} />
<RectangleSkeleton width="111px" height="22px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<RectangleSkeleton className="header" width={"34px"} height={"16px"} />
<RectangleSkeleton className="header" width="34px" height="22px" />
<RectangleSkeleton
className="header header-last"
width={"37px"}
height={"16px"}
width="37px"
height="22px"
/>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width={"98px"}
height={"14px"}
width="98px"
height="16px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"200px"}
height={"12px"}
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"230px"}
height={"12px"}
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width={"98px"}
height={"14px"}
width="98px"
height="16px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"200px"}
height={"12px"}
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"230px"}
height={"12px"}
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width={"98px"}
height={"14px"}
width="98px"
height="16px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"200px"}
height={"12px"}
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"230px"}
height={"12px"}
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width={"98px"}
height={"14px"}
width="98px"
height="16px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"200px"}
height={"12px"}
className="scope-desc-loader"
width="200px"
height="17px"
/>
<RectangleSkeleton
className={"scope-desc-loader"}
width={"230px"}
height={"12px"}
className="scope-desc-loader"
width="230px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
<StyledScopesCheckbox>
<CheckboxSkeleton />
</StyledScopesCheckbox>{" "}
<StyledScopesName>
<RectangleSkeleton
className="scope-name-loader"
width="98px"
height="16px"
/>
<RectangleSkeleton
className="scope-desc-loader"
width="200px"
height="17px"
/>
</StyledScopesName>
<StyledScopesCheckbox>
<CheckboxSkeleton className="checkbox-read" />
</StyledScopesCheckbox>
</StyledScopesContainer>
<StyledBlock>
<StyledHeaderRow>
<RectangleSkeleton width={"162px"} height={"16px"} />
<RectangleSkeleton width="162px" height="22px" />
</StyledHeaderRow>
<StyledInputBlock>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"114px"} height={"13px"} />
<RectangleSkeleton width="114px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width={"100%"} height={"32px"} />
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
<StyledInputGroup>
<StyledHeaderRow>
<RectangleSkeleton width={"96px"} height={"13px"} />
<RectangleSkeleton width="96px" height="20px" />
<HelpButtonSkeleton />
</StyledHeaderRow>
<StyledInputRow>
<RectangleSkeleton width={"100%"} height={"32px"} />
<RectangleSkeleton width="100%" height="32px" />
</StyledInputRow>
</StyledInputGroup>
</StyledInputBlock>

View File

@ -2,10 +2,10 @@ import React from "react";
import { Trans } from "react-i18next";
import { TTranslation } from "@docspace/shared/types";
import { HelpButton } from "@docspace/shared/components/help-button";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/interfaces";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
@ -23,7 +23,7 @@ interface BasicBlockProps {
descriptionValue: string;
allowPkce: boolean;
changeValue: (name: string, value: string | boolean) => void;
changeValue: (name: keyof IClientReqDTO, value: string | boolean) => void;
isEdit: boolean;
errorFields: string[];
@ -32,12 +32,12 @@ interface BasicBlockProps {
}
function getImageDimensions(
image: any
image: HTMLImageElement,
): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
image.onload = function (e: any) {
const width = this.width;
const height = this.height;
return new Promise((resolve) => {
image.onload = () => {
const width = image.width;
const height = image.height;
resolve({ height, width });
};
});
@ -47,9 +47,9 @@ function compressImage(
image: HTMLImageElement,
scale: number,
initialWidth: number,
initialHeight: number
): any {
return new Promise((resolve, reject) => {
initialHeight: number,
): Promise<Blob | undefined | null> {
return new Promise((resolve) => {
const canvas = document.createElement("canvas");
canvas.width = scale * initialWidth;
@ -81,11 +81,11 @@ const BasicBlock = ({
onBlur,
}: BasicBlockProps) => {
const onChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const target = e.target;
changeValue(target.name, target.value);
changeValue(target.name as keyof IClientReqDTO, target.value);
};
const onSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
@ -94,43 +94,45 @@ const BasicBlock = ({
if (file) {
const imgEl = document.getElementsByClassName(
"client-logo"
"client-logo",
)[0] as HTMLImageElement;
imgEl.src = URL.createObjectURL(file);
const { height, width } = await getImageDimensions(imgEl);
const MAX_WIDTH = 32; //if we resize by width, this is the max width of compressed image
const MAX_HEIGHT = 32; //if we resize by height, this is the max height of the compressed image
const MAX_WIDTH = 32; // if we resize by width, this is the max width of compressed image
const MAX_HEIGHT = 32; // if we resize by height, this is the max height of the compressed image
const widthRatioBlob = await compressImage(
imgEl,
MAX_WIDTH / width,
width,
height
height,
);
const heightRatioBlob = await compressImage(
imgEl,
MAX_HEIGHT / height,
width,
height
height,
);
//pick the smaller blob between both
const compressedBlob =
widthRatioBlob.size > heightRatioBlob.size
? heightRatioBlob
: widthRatioBlob;
if (widthRatioBlob && heightRatioBlob) {
// pick the smaller blob between both
const compressedBlob =
widthRatioBlob.size > heightRatioBlob.size
? heightRatioBlob
: widthRatioBlob;
const reader = new FileReader();
reader.readAsDataURL(compressedBlob);
const reader = new FileReader();
reader.readAsDataURL(compressedBlob);
reader.onload = () => {
const result = reader.result as string;
reader.onload = () => {
const result = reader.result as string;
changeValue("logo", result);
};
changeValue("logo", result);
};
}
}
};
@ -146,11 +148,11 @@ const BasicBlock = ({
return (
<StyledBlock>
<BlockHeader header={"Basic info"} />
<BlockHeader header="Basic info" />
<StyledInputBlock>
<InputGroup
label={t("AppName")}
name={"name"}
name="name"
placeholder={t("Common:EnterName")}
value={nameValue}
error={isNameError ? `${t("ErrorName")} 3` : t("ThisRequiredField")}
@ -161,7 +163,7 @@ const BasicBlock = ({
/>
<InputGroup
label={t("WebsiteUrl")}
name={"website_url"}
name="website_url"
placeholder={t("EnterURL")}
value={websiteUrlValue}
error={
@ -193,7 +195,7 @@ const BasicBlock = ({
<TextAreaGroup
label={t("Common:Description")}
name={"description"}
name="description"
placeholder={t("EnterDescription")}
value={descriptionValue}
onChange={onChange}
@ -201,13 +203,13 @@ const BasicBlock = ({
/>
<InputGroup
label={t("AuthenticationMethod")}
name={"website_url"}
name="website_url"
placeholder={t("EnterURL")}
value={websiteUrlValue}
error=""
onChange={() => {}}
>
<div className={"pkce"}>
<div className="pkce">
<Checkbox
label={t("AllowPKCE")}
isChecked={allowPkce}

View File

@ -17,14 +17,14 @@ const BlockHeader = ({
return (
<StyledHeaderRow className={className}>
<Text
fontSize={"16px"}
fontSize="16px"
fontWeight={700}
lineHeight={"22px"}
lineHeight="22px"
title={header}
tag={""}
as={"p"}
color={""}
textAlign={""}
tag=""
as="p"
color=""
textAlign=""
>
{header}
</Text>

View File

@ -35,7 +35,6 @@ const ButtonsBlock = ({
return (
<StyledButtonContainer>
<Button
// @ts-ignore
label={saveLabel}
isLoading={isRequestRunning}
isDisabled={saveButtonDisabled}
@ -46,7 +45,6 @@ const ButtonsBlock = ({
/>
<Button
// @ts-ignore
label={cancelLabel}
isDisabled={cancelButtonDisabled}
size={buttonSize}

View File

@ -3,6 +3,7 @@ import { Trans } from "react-i18next";
import copy from "copy-to-clipboard";
import { toastr } from "@docspace/shared/components/toast";
import { TTranslation } from "@docspace/shared/types";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
@ -10,7 +11,7 @@ import BlockHeader from "./BlockHeader";
import InputGroup from "./InputGroup";
interface ClientBlockProps {
t: any;
t: TTranslation;
idValue: string;
secretValue: string;
@ -33,7 +34,7 @@ const ClientBlock = ({
setValue({ id: idValue, secret: secretValue });
}, [idValue, secretValue]);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {};
const onChange = () => {};
const onCopyClick = (name: string) => {
if (name === "id") {
@ -53,20 +54,20 @@ const ClientBlock = ({
<StyledInputBlock>
<InputGroup
label={t("ID")}
name={""}
placeholder={""}
name=""
placeholder=""
value={value.id}
error={""}
error=""
onChange={onChange}
withCopy
onCopyClick={() => onCopyClick("id")}
/>
<InputGroup
label={t("Secret")}
name={""}
placeholder={""}
name=""
placeholder=""
value={value.secret}
error={""}
error=""
onChange={onChange}
withCopy
isPassword

View File

@ -88,15 +88,13 @@ const InputGroup = ({
removeMargin
hasError={isError}
>
{children ? (
children
) : (
{children || (
<>
{isRequestRunning ? (
<RectangleSkeleton
className={"loader"}
width={"100%"}
height={"32px"}
className="loader"
width="100%"
height="32px"
/>
) : (
<InputBlock

View File

@ -1,27 +1,34 @@
import React from "react";
import { InputBlock } from "@docspace/shared/components/input-block";
import { Text } from "@docspace/shared/components/text";
import { SelectorAddButton } from "@docspace/shared/components/selector-add-button";
import { SelectedItem } from "@docspace/shared/components/selected-item";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TTranslation } from "@docspace/shared/types";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/interfaces";
import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg";
import {
StyledChipsContainer,
StyledInputAddBlock,
StyledInputGroup,
StyledInputRow,
} from "../ClientForm.styled";
import { isValidUrl } from "../ClientForm.utils";
import InputGroup from "./InputGroup";
import { isValidUrl } from "..";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
interface MultiInputGroupProps {
t: any;
t: TTranslation;
label: string;
name: string;
placeholder: string;
currentValue: string[];
hasError?: boolean;
onAdd: (name: string, value: string, remove?: boolean) => void;
onAdd: (name: keyof IClientReqDTO, value: string, remove?: boolean) => void;
helpButtonText?: string;
@ -41,15 +48,30 @@ const MultiInputGroup = ({
}: MultiInputGroupProps) => {
const [value, setValue] = React.useState("");
const [isFocus, setIsFocus] = React.useState(false);
const [isAddVisible, setIsAddVisible] = React.useState(false);
const [isError, setIsError] = React.useState(false);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
const addRef = React.useRef<null | HTMLDivElement>(null);
setValue(value);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setValue(v);
if (isValidUrl(v)) {
setIsAddVisible(true);
} else {
setIsAddVisible(false);
}
};
const onFocus = () => {
setIsFocus(true);
};
const onBlur = () => {
setIsFocus(false);
if (value) {
if (isValidUrl(value)) {
setIsError(false);
@ -61,6 +83,42 @@ const MultiInputGroup = ({
}
};
const onAddAction = React.useCallback(() => {
if (isDisabled || isError) return;
onAdd(name as keyof IClientReqDTO, value);
setIsAddVisible(false);
setIsError(false);
setValue("");
}, [isDisabled, isError, name, onAdd, value]);
React.useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && isAddVisible) {
onAddAction();
}
};
if (isFocus) {
window.addEventListener("keydown", onKeyDown);
} else {
window.removeEventListener("keydown", onKeyDown);
}
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isAddVisible, isFocus, onAddAction]);
React.useEffect(() => {
if (!addRef.current) return;
if (isAddVisible) {
addRef.current.style.display = "flex";
} else {
addRef.current.style.display = "none";
}
}, [isAddVisible]);
return (
<StyledInputGroup>
<InputGroup
@ -88,33 +146,41 @@ const MultiInputGroup = ({
tabIndex={0}
maxLength={255}
isDisabled={isDisabled}
onFocus={onFocus}
onBlur={onBlur}
hasError={isError || hasError}
size={InputSize.base}
type={InputType.text}
/>
<StyledInputAddBlock ref={addRef} onClick={onAddAction}>
<Text fontSize="13px" fontWeight={600} lineHeight="20px" truncate>
{value}
</Text>
<div className="add-block">
<Text fontSize="13px" fontWeight={400} lineHeight="20px" truncate>
{t("Common:AddButton")}
</Text>
<ArrowIcon />
</div>
</StyledInputAddBlock>
<SelectorAddButton
onClick={() => {
if (isDisabled || isError) return;
onAdd(name, value);
setValue("");
}}
onClick={onAddAction}
isDisabled={isDisabled || isError}
/>
</StyledInputRow>
</InputGroup>
<StyledChipsContainer>
{currentValue.map((v, index) => (
{currentValue.map((v) => (
<SelectedItem
key={`${v}-${index}`}
key={`${v}`}
propKey={v}
isInline
label={v}
isDisabled={isDisabled}
hideCross={isDisabled}
onClose={() => {
!isDisabled && onAdd(name, v);
if (!isDisabled) onAdd(name as keyof IClientReqDTO, v, true);
}}
/>
))}

View File

@ -1,16 +1,24 @@
import React from "react";
import { TTranslation } from "@docspace/shared/types";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/interfaces";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
import BlockHeader from "./BlockHeader";
import MultiInputGroup from "./MultiInputGroup";
interface OAuthBlockProps {
t: any;
t: TTranslation;
redirectUrisValue: string[];
allowedOriginsValue: string[];
changeValue: (name: string, value: string) => void;
changeValue: (
name: keyof IClientReqDTO,
value: string,
remove?: boolean,
) => void;
requiredErrorFields: string[];
isEdit: boolean;
@ -33,7 +41,7 @@ const OAuthBlock = ({
t={t}
label={t("RedirectsURLS")}
placeholder={t("EnterURL")}
name={"redirect_uris"}
name="redirect_uris"
onAdd={changeValue}
currentValue={redirectUrisValue}
helpButtonText={t("RedirectsURLSHelpButton")}
@ -44,7 +52,7 @@ const OAuthBlock = ({
t={t}
label={t("AllowedOrigins")}
placeholder={t("EnterURL")}
name={"allowed_origins"}
name="allowed_origins"
onAdd={changeValue}
currentValue={allowedOriginsValue}
helpButtonText={t("AllowedOriginsHelpButton")}

View File

@ -1,6 +1,7 @@
import React from "react";
import {
IClientReqDTO,
IFilteredScopes,
IScope,
} from "@docspace/shared/utils/oauth/interfaces";
@ -9,7 +10,7 @@ import {
getScopeTKeyName,
} from "@docspace/shared/utils/oauth";
import { ScopeGroup, ScopeType } from "@docspace/shared/enums";
import { TTranslation } from "@docspace/shared/types";
import { Text } from "@docspace/shared/components/text";
import { Checkbox } from "@docspace/shared/components/checkbox";
@ -24,8 +25,8 @@ import {
interface IScopesBlockProps {
scopes: IScope[];
selectedScopes: string[];
onAddScope: (name: string, scope: string) => void;
t: any;
onAddScope: (name: keyof IClientReqDTO, scope: string) => void;
t: TTranslation;
isEdit: boolean;
}
@ -38,7 +39,7 @@ const ScopesBlock = ({
}: IScopesBlockProps) => {
const [checkedScopes, setCheckedScopes] = React.useState<string[]>([]);
const [filteredScopes, setFilteredScopes] = React.useState<IFilteredScopes>(
filterScopeByGroup(selectedScopes, scopes)
filterScopeByGroup(selectedScopes, scopes),
);
React.useEffect(() => {
@ -46,12 +47,12 @@ const ScopesBlock = ({
setCheckedScopes([...selectedScopes]);
setFilteredScopes({ ...filtered });
}, [selectedScopes]);
}, [scopes, selectedScopes]);
const onAddCheckedScope = (
group: ScopeGroup,
type: ScopeType,
name: string = ""
name: string = "",
) => {
const isChecked = checkedScopes.includes(name);
@ -74,7 +75,7 @@ const ScopesBlock = ({
} else {
setFilteredScopes((val) => {
const isReadChecked = checkedScopes.includes(
val[group].read?.name || ""
val[group].read?.name || "",
);
val[group].isChecked = isReadChecked;
@ -91,67 +92,63 @@ const ScopesBlock = ({
};
const getRenderedScopeList = () => {
const list = [];
const list: React.ReactNode[] = [];
for (let key in filteredScopes) {
Object.entries(filteredScopes).forEach(([key, value]) => {
const name = getScopeTKeyName(key as ScopeGroup);
const scope = filteredScopes[key];
const isReadDisabled = scope.checkedType === ScopeType.write;
const isReadChecked = scope.isChecked;
const isReadDisabled = value.checkedType === ScopeType.write;
const isReadChecked = value.isChecked;
const row = (
<React.Fragment key={name}>
<StyledScopesName>
<Text
className="scope-name"
fontSize={"14px"}
fontSize="14px"
fontWeight={600}
lineHeight={"16px"}
lineHeight="16px"
>
{t(`Common:${name}`)}
</Text>
{scope.read?.name && (
{value.read?.name && (
<Text
className={"scope-desc"}
fontSize={"12px"}
className="scope-desc"
fontSize="12px"
fontWeight={400}
lineHeight={"16px"}
lineHeight="16px"
>
<Text
className={"scope-desc"}
as={"span"}
fontSize={"12px"}
className="scope-desc"
as="span"
fontSize="12px"
fontWeight={600}
lineHeight={"16px"}
lineHeight="16px"
>
{scope.read?.name}
{value.read?.name}
</Text>{" "}
{t(`Common:${scope.read?.tKey}`)}
{t(`Common:${value.read?.tKey}`)}
</Text>
)}
{scope.write?.name && (
<>
{value.write?.name && (
<Text
className="scope-desc"
fontSize="12px"
fontWeight={400}
lineHeight="16px"
>
<Text
className={"scope-desc"}
fontSize={"12px"}
fontWeight={400}
lineHeight={"16px"}
className="scope-desc"
as="span"
fontSize="12px"
fontWeight={600}
lineHeight="16px"
>
<Text
className={"scope-desc"}
as={"span"}
fontSize={"12px"}
fontWeight={600}
lineHeight={"16px"}
>
{scope.write?.name}
</Text>{" "}
{t(`Common:${scope.write?.tKey}`)}
</Text>
</>
{value.write?.name}
</Text>{" "}
{t(`Common:${value.write?.tKey}`)}
</Text>
)}
</StyledScopesName>
<StyledScopesCheckbox>
@ -163,21 +160,21 @@ const ScopesBlock = ({
onAddCheckedScope(
key as ScopeGroup,
ScopeType.read,
scope.read?.name
value.read?.name,
)
}
/>
</StyledScopesCheckbox>
<StyledScopesCheckbox>
{scope.write?.name && (
{value.write?.name && (
<Checkbox
isChecked={isReadDisabled}
isDisabled={isEdit || !scope.write?.name}
isDisabled={isEdit || !value.write?.name}
onChange={() =>
onAddCheckedScope(
key as ScopeGroup,
ScopeType.write,
scope.write?.name
value.write?.name,
)
}
/>
@ -187,7 +184,8 @@ const ScopesBlock = ({
);
list.push(row);
}
});
return list;
};
@ -203,18 +201,18 @@ const ScopesBlock = ({
<Text
className="header"
fontSize={"14px"}
fontSize="14px"
fontWeight={600}
lineHeight={"22px"}
lineHeight="22px"
>
{t("Read")}
</Text>
<Text
className="header header-last"
fontSize={"14px"}
fontSize="14px"
fontWeight={600}
lineHeight={"22px"}
lineHeight="22px"
>
{t("Write")}
</Text>

View File

@ -46,14 +46,14 @@ const SelectGroup = ({
<StyledInputGroup>
<div className="label">
<Text
fontSize={"13px"}
fontSize="13px"
fontWeight={600}
lineHeight={"20px"}
title={""}
tag={""}
as={"p"}
color={""}
textAlign={""}
lineHeight="20px"
title=""
tag=""
as="p"
color=""
textAlign=""
>
{label} *
</Text>
@ -61,32 +61,33 @@ const SelectGroup = ({
<div className="select">
<img
className="client-logo"
style={{ display: !!value ? "block" : "none" }}
style={{ display: value ? "block" : "none" }}
alt="img"
src={value}
/>
<SelectorAddButton onClick={onClick} />
<Text
fontSize={"13px"}
fontSize="13px"
fontWeight={600}
lineHeight={"20px"}
title={""}
tag={""}
as={"p"}
color={""}
textAlign={""}
lineHeight="20px"
title=""
tag=""
as="p"
color=""
textAlign=""
>
{selectLabel}
</Text>
</div>
<Text
fontSize={"12px"}
fontSize="12px"
fontWeight={600}
lineHeight={"16px"}
title={""}
tag={""}
as={"p"}
color={""}
textAlign={""}
lineHeight="16px"
title=""
tag=""
as="p"
color=""
textAlign=""
className="description"
>
{description}

View File

@ -1,16 +1,20 @@
import React from "react";
import { TTranslation } from "@docspace/shared/types";
import { IClientReqDTO } from "@docspace/shared/utils/oauth/interfaces";
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
import BlockHeader from "./BlockHeader";
import InputGroup from "./InputGroup";
interface SupportBlockProps {
t: any;
t: TTranslation;
policyUrlValue: string;
termsUrlValue: string;
changeValue: (name: string, value: string) => void;
changeValue: (name: keyof IClientReqDTO, value: string) => void;
isEdit: boolean;
errorFields: string[];
@ -33,7 +37,7 @@ const SupportBlock = ({
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target;
changeValue(target.name, target.value);
changeValue(target.name as keyof IClientReqDTO, target.value);
};
const policyRequiredError = requiredErrorFields.includes("policy_url");
@ -47,7 +51,7 @@ const SupportBlock = ({
<StyledInputBlock>
<InputGroup
label={t("PrivacyPolicyURL")}
name={"policy_url"}
name="policy_url"
placeholder={t("EnterURL")}
value={policyUrlValue}
error={
@ -64,7 +68,7 @@ const SupportBlock = ({
/>
<InputGroup
label={t("TermsOfServiceURL")}
name={"terms_url"}
name="terms_url"
placeholder={t("EnterURL")}
value={termsUrlValue}
error={
@ -73,7 +77,7 @@ const SupportBlock = ({
: t("ThisRequiredField")
}
onChange={onChange}
helpButtonText={t("TermsOfServiceURLHelpButton")}
helpButtonText={t("TermsOfServiceURLHelpButton")}
disabled={isEdit}
isRequired
isError={termsError || termsRequiredError}

View File

@ -7,7 +7,6 @@ import { StyledInputGroup } from "../ClientForm.styled";
interface TextAreaProps {
label: string;
name: string;
value: string;
placeholder: string;
@ -29,14 +28,14 @@ const TextAreaGroup = ({
<StyledInputGroup>
<div className="label">
<Text
fontSize={"13px"}
fontSize="13px"
fontWeight={600}
lineHeight={"20px"}
title={""}
tag={""}
as={"p"}
color={""}
textAlign={""}
lineHeight="20px"
title=""
tag=""
as="p"
color=""
textAlign=""
>
{label}
</Text>

View File

@ -6,8 +6,12 @@ import { useTranslation } from "react-i18next";
import {
IClientProps,
IClientReqDTO,
INoAuthClientProps,
} from "@docspace/shared/utils/oauth/interfaces";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { getClient } from "@docspace/shared/api/oauth";
import ResetDialog from "../ResetDialog";
@ -19,18 +23,10 @@ import ScopesBlock from "./components/ScopesBlock";
import ButtonsBlock from "./components/ButtonsBlock";
import { StyledContainer } from "./ClientForm.styled";
import { ClientFormProps, ClientStore } from "./ClientForm.types";
import ClientFormLoader from "./Loader";
import { isValidUrl } from "./ClientForm.utils";
export function isValidUrl(url: string) {
try {
new URL(url);
return true;
} catch (err) {
return false;
}
}
import ClientFormLoader from "./Loader";
const ClientForm = ({
id,
@ -39,17 +35,16 @@ const ClientForm = ({
scopeList,
fetchClient,
fetchScopes,
saveClient,
updateClient,
setResetDialogVisible,
resetDialogVisible,
setResetDialogVisible,
setClientSecretProps,
clientSecretProps,
setClientSecretProps,
currentDeviceType,
}: ClientFormProps) => {
@ -59,9 +54,9 @@ const ClientForm = ({
const [isRequestRunning, setIsRequestRunning] =
React.useState<boolean>(false);
const [initialClient, setInitialClient] = React.useState<IClientProps>(
{} as IClientProps
);
const [initialClient, setInitialClient] = React.useState<
IClientProps | INoAuthClientProps
>({} as IClientProps);
const [form, setForm] = React.useState<IClientReqDTO>({
name: "",
logo: "",
@ -99,44 +94,34 @@ const ClientForm = ({
}
}, [clientSecretProps, setClientSecretProps]);
const onCancelClick = () => {
navigate("/portal-settings/developer-tools/oauth");
};
const onSaveClick = async () => {
try {
if (!id) {
let isValid = true;
for (let key in form) {
switch (key) {
case "name":
case "logo":
case "website_url":
case "terms_url":
case "policy_url":
if (form[key] === "") {
if (!requiredErrorFields.includes(key))
setRequiredErrorFields((s) => [...s, key]);
console.log(key);
isValid = false;
}
isValid = isValid && !errorFields.includes(key);
Object.entries(form).forEach(([key, value]) => {
if (key === "description" || key === "logout_redirect_uri") return;
break;
if (
(value === "" && typeof value === "string") ||
(value.length === 0 && value instanceof Array)
) {
if (!requiredErrorFields.includes(key))
setRequiredErrorFields((s) => [...s, key]);
case "redirect_uris":
case "allowed_origins":
case "scopes":
if (form[key].length === 0) {
if (!requiredErrorFields.includes(key))
setRequiredErrorFields((s) => [...s, key]);
isValid = false;
}
isValid = isValid && !errorFields.includes(key);
console.log(key);
break;
isValid = false;
}
}
console.log(isValid);
isValid = isValid && !errorFields.includes(key);
if (key === "website_url" && !isValidUrl(value)) {
isValid = false;
}
});
if (!isValid) return;
@ -149,117 +134,107 @@ const ClientForm = ({
onCancelClick();
} catch (e) {
console.log(e);
toastr.error(e as unknown as TData);
}
};
const onCancelClick = () => {
navigate("/portal-settings/developer-tools/oauth");
};
const onResetClick = React.useCallback(async () => {
if (!setResetDialogVisible) return;
setResetDialogVisible(true);
setResetDialogVisible?.(true);
}, [setResetDialogVisible]);
// setClientSecret(newSecret);
}, [clientId, setResetDialogVisible]);
const onChangeForm = (name: string, value: string | boolean) => {
const onChangeForm = (
name: keyof IClientReqDTO,
value: string | boolean,
remove?: boolean,
) => {
setForm((val) => {
const newVal = { ...val };
if (!(name in val)) return val;
if (newVal[name as keyof IClientReqDTO] instanceof Array) {
//@ts-ignore
if (newVal[name as keyof IClientReqDTO].includes(value)) {
//@ts-ignore
newVal[name as keyof IClientReqDTO] = newVal[
name as keyof IClientReqDTO
//@ts-ignore
].filter((v: string) => v !== value);
} else {
//@ts-ignore
newVal[name as keyof IClientReqDTO].push(value);
const newVal: IClientReqDTO = { ...val };
let item = newVal[name];
if (typeof value === "string" && item instanceof Array) {
if (item.includes(value) && remove) {
item = item.filter((v: string) => v !== value);
} else if (!item.includes(value)) {
item.push(value);
}
} else {
//@ts-ignore
newVal[name as keyof IClientReqDTO] = value;
item = value;
}
function updateForm<K extends keyof IClientReqDTO>(
key: K,
v: IClientReqDTO[K],
) {
newVal[key] = v;
}
updateForm(name, item);
return { ...newVal };
});
};
const getClientData = React.useCallback(async () => {
if (!fetchScopes || !fetchClient) return;
if (clientId) return;
const actions = [];
if (id && !client) {
actions.push(fetchClient(id));
actions.push(getClient(id));
}
if (scopeList?.length === 0) actions.push(fetchScopes());
if (scopeList?.length === 0) actions.push(fetchScopes?.());
try {
const [fetchedClient, ...rest] = await Promise.all(actions);
if (actions.length > 0) setIsLoading(true);
if (id) {
const [fetchedClient] = await Promise.all(actions);
const item = fetchedClient ?? client;
if (id && item) {
setForm({
name: fetchedClient?.name || client?.name || "",
logo: fetchedClient?.logo || client?.logo || "",
website_url: fetchedClient?.websiteUrl || client?.websiteUrl || "",
description: fetchedClient?.description || client?.description || "",
name: item.name,
logo: item.logo,
website_url: item.websiteUrl,
description: item.description ?? "",
redirect_uris: fetchedClient?.redirectUris
? [...fetchedClient?.redirectUris]
: client?.redirectUris
? [...client?.redirectUris]
: [],
allowed_origins: fetchedClient?.allowedOrigins
? [...fetchedClient.allowedOrigins]
: client?.allowedOrigins
? [...client.allowedOrigins]
: [],
logout_redirect_uri:
fetchedClient?.logoutRedirectUri || client?.logoutRedirectUri || "",
redirect_uris: item.redirectUris ? [...item.redirectUris] : [],
allowed_origins: item.allowedOrigins ? [...item.allowedOrigins] : [],
logout_redirect_uri: item.logoutRedirectUri ?? "",
terms_url: fetchedClient?.termsUrl || client?.termsUrl || "",
policy_url: fetchedClient?.policyUrl || client?.policyUrl || "",
terms_url: item.termsUrl ?? "",
policy_url: item.policyUrl ?? "",
allow_pkce:
fetchedClient?.authenticationMethods.includes(
AuthenticationMethod.none
) ||
client?.authenticationMethods.includes(AuthenticationMethod.none) ||
false,
allow_pkce: item.authenticationMethods
? item.authenticationMethods.includes(AuthenticationMethod.none)
: false,
scopes: fetchedClient?.scopes
? [...fetchedClient.scopes]
: client?.scopes
? [...client.scopes]
: [],
scopes: item.scopes ? [...item.scopes] : [],
});
setClientId(fetchedClient?.clientId || client?.clientId || "");
setClientSecret(
fetchedClient?.clientSecret || client?.clientSecret || ""
);
setClientId(item.clientId ?? " ");
setClientSecret(item.clientSecret ?? " ");
setInitialClient(client || fetchedClient || ({} as IClientProps));
setInitialClient(item ?? ({} as IClientProps));
}
setIsLoading(false);
setTimeout(() => {
setIsLoading(false);
}, 500);
} catch (e) {
setIsLoading(false);
console.log(e);
toastr.error(e as unknown as TData);
}
}, [id, fetchScopes]);
}, [clientId, id, client, scopeList?.length, fetchScopes]);
React.useEffect(() => {
setIsLoading(true);
getClientData();
}, [getClientData, fetchScopes]);
}, [getClientData]);
const onBlur = (key: string) => {
if (
@ -287,29 +262,31 @@ const ClientForm = ({
let isValid = true;
if (isEdit) {
for (let key in form) {
Object.entries(form).forEach(([key, value]) => {
switch (key) {
case "name":
isValid = isValid && !!form[key];
isValid = isValid && !!value;
if (
form[key] &&
value &&
!errorFields.includes(key) &&
(form[key].length < 3 || form[key].length > 256)
(value.length < 3 || value.length > 256)
) {
isValid = false;
return setErrorFields((value) => {
return [...value, key];
setErrorFields((val) => {
return [...val, key];
});
return;
}
if (
errorFields.includes(key) &&
(!form[key] || (form[key].length > 2 && form[key].length < 256))
(!value || (value.length > 2 && value.length < 257))
) {
setErrorFields((value) => {
return value.filter((n) => n !== key);
setErrorFields((val) => {
return val.filter((n) => n !== key);
});
return;
@ -318,8 +295,11 @@ const ClientForm = ({
isValid = isValid && !errorFields.includes(key);
break;
default:
break;
}
}
});
return (
isValid &&
@ -332,41 +312,48 @@ const ClientForm = ({
form.allowed_origins.length !== initialClient.allowedOrigins.length ||
form.allow_pkce !==
initialClient.authenticationMethods.includes(
AuthenticationMethod.none
AuthenticationMethod.none,
))
);
}
for (let key in form) {
Object.entries(form).forEach(([key, value]) => {
switch (key) {
case "name":
case "logo":
case "website_url":
case "terms_url":
case "policy_url":
case "website_url":
if (
errorFields.includes(key) &&
(!form[key] || (form[key].length > 2 && form[key].length < 256))
(!value || (value.length > 2 && value.length < 256))
) {
setErrorFields((value) => {
return value.filter((n) => n !== key);
});
if (
(key === "website_url" && isValidUrl(value)) ||
key !== "website_url"
)
setErrorFields((val) => {
return val.filter((n) => n !== key);
});
}
if (requiredErrorFields.includes(key) && form[key] !== "")
setRequiredErrorFields((value) => value.filter((v) => v !== key));
if (requiredErrorFields.includes(key) && value !== "")
setRequiredErrorFields((val) => val.filter((v) => v !== key));
break;
case "redirect_uris":
case "allowed_origins":
case "scopes":
if (requiredErrorFields.includes(key) && form[key].length > 0)
setRequiredErrorFields((value) => value.filter((v) => v !== key));
if (requiredErrorFields.includes(key) && value.length > 0)
setRequiredErrorFields((val) => val.filter((v) => v !== key));
break;
default:
break;
}
}
});
return isValid;
};
@ -453,7 +440,6 @@ export default inject(
clientList,
scopeList,
fetchClient,
fetchScopes,
saveClient,
@ -471,7 +457,6 @@ export default inject(
const props: ClientFormProps = {
scopeList,
fetchClient,
fetchScopes,
saveClient,
@ -485,13 +470,11 @@ export default inject(
};
if (id) {
const client = clientList.find(
(client: IClientProps) => client.clientId === id
);
const client = clientList.find((c: IClientProps) => c.clientId === id);
props.client = client;
}
return { ...props };
}
},
)(observer(ClientForm));

View File

@ -1,4 +1,5 @@
import { TTranslation } from "@docspace/shared/types";
export interface EmptyScreenProps {
t: TTranslation;
}

View File

@ -5,11 +5,12 @@ import EmptyScreenOauthSvgUrl from "PUBLIC_DIR/images/empty_screen_oauth.svg?url
import RegisterNewButton from "../RegisterNewButton";
import { EmptyScreenProps } from "./EmptyScreen.types";
const OAuthEmptyScreen = ({ t }: EmptyScreenProps) => {
return (
<EmptyScreenContainer
imageSrc={EmptyScreenOauthSvgUrl}
imageAlt={"Empty oauth list"}
imageAlt="Empty oauth list"
headerText={t("NoOAuthAppHeader")}
subheadingText={t("OAuthAppDescription")}
buttons={<RegisterNewButton t={t} />}

View File

@ -25,31 +25,27 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useState, useTransition, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import styled, { css } from "styled-components";
import { Submenu } from "@docspace/shared/components/submenu";
import { Box } from "@docspace/shared/components/box";
import { inject, observer } from "mobx-react";
import { Submenu } from "@docspace/shared/components/submenu";
import { Box } from "@docspace/shared/components/box";
import AppLoader from "@docspace/shared/components/app-loader";
import { Badge } from "@docspace/shared/components/badge";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import config from "PACKAGE_FILE";
import { useNavigate } from "react-router-dom";
import JavascriptSDK from "./JavascriptSDK";
import Webhooks from "./Webhooks";
import Api from "./Api";
import { useTranslation } from "react-i18next";
import { isMobile, isMobileOnly } from "react-device-detect";
import AppLoader from "@docspace/shared/components/app-loader";
import SSOLoader from "./sub-components/ssoLoader";
import { WebhookConfigsLoader } from "./Webhooks/sub-components/Loaders";
import OAuth from "./OAuth";
import { DeviceType } from "@docspace/shared/enums";
import PluginSDK from "./PluginSDK";
import { Badge } from "@docspace/shared/components/badge";
import OAuth from "./OAuth";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
import SSOLoader from "./sub-components/ssoLoader";
const StyledSubmenu = styled(Submenu)`
.sticky {
@ -115,6 +111,11 @@ const DeveloperToolsWrapper = (props) => {
name: t("Webhooks:Webhooks"),
content: <Webhooks />,
},
{
id: "oauth",
name: t("OAuth:Oauth"),
content: <OAuth />,
},
];
const [currentTab, setCurrentTab] = useState(

View File

@ -507,6 +507,14 @@ export const settingsTree = [
tKey: "Common:DeveloperTools",
isCategory: true,
},
{
id: "portal-settings_catalog-oauth",
key: "7-4",
icon: "",
link: "oauth",
tKey: "OAuth:OAuth",
isCategory: true,
},
],
},
{

View File

@ -2,41 +2,35 @@ import { makeAutoObservable, runInAction } from "mobx";
import {
addClient,
getClient,
updateClient,
changeClientStatus,
regenerateSecret,
deleteClient,
getClientList,
getScope,
getScopeList,
getConsentList,
revokeUserClient,
} from "@docspace/shared/api/oauth";
import {
IClientListProps,
IClientProps,
IClientReqDTO,
INoAuthClientProps,
IScope,
} from "@docspace/shared/utils/oauth/interfaces";
import { toastr } from "@docspace/shared/components/toast";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { UserStore } from "@docspace/shared/store/UserStore";
import { TTranslation } from "@docspace/shared/types";
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";
import PencilReactSvgUrl from "PUBLIC_DIR/images/pencil.react.svg?url";
import CodeReactSvgUrl from "PUBLIC_DIR/images/code.react.svg?url";
import ExternalLinkReactSvgUrl from "PUBLIC_DIR/images/external.link.react.svg?url";
import OauthRevokeSvgUrl from "PUBLIC_DIR/images/oauth.revoke.svg?url";
import { transformToClientProps } from "@docspace/shared/utils/oauth";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { UserStore } from "@docspace/shared/store/UserStore";
import { TTranslation } from "@docspace/shared/types";
import SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
const PAGE_LIMIT = 100;
@ -76,9 +70,7 @@ export interface OAuthStoreProps {
editClient: (clientId: string) => void;
clients: IClientProps[];
fetchClient: (
clientId: string
) => Promise<IClientProps | INoAuthClientProps | undefined>;
fetchClients: () => Promise<void>;
fetchNextClients: (startIndex: number) => Promise<void>;
@ -113,17 +105,14 @@ export interface OAuthStoreProps {
setActiveClient: (clientId: string) => void;
scopes: IScope[];
fetchScope: (name: string) => Promise<IScope>;
fetchScopes: () => Promise<void>;
getContextMenuItems: (
t: TTranslation,
item: IClientProps,
isInfo?: boolean,
isSettings?: boolean
) => {
[key: string]: any | string | boolean | ((clientId: string) => void);
}[];
isSettings?: boolean,
) => ContextMenuModel[];
clientList: IClientProps[];
isEmptyClientList: boolean;
@ -137,13 +126,19 @@ class OAuthStore implements OAuthStoreProps {
viewAs: ViewAsType = "table";
currentPage: number = 0;
nextPage: number | null = null;
itemCount: number = 0;
infoDialogVisible: boolean = false;
previewDialogVisible: boolean = false;
disableDialogVisible: boolean = false;
deleteDialogVisible: boolean = false;
resetDialogVisible: boolean = false;
selection: string[] = [];
@ -210,12 +205,10 @@ class OAuthStore implements OAuthStoreProps {
setSelection = (clientId: string) => {
if (!clientId) {
this.selection = [];
} else if (this.selection.includes(clientId)) {
this.selection = this.selection.filter((s) => s !== clientId);
} else {
if (this.selection.includes(clientId)) {
this.selection = this.selection.filter((s) => s !== clientId);
} else {
this.selection.push(clientId);
}
this.selection.push(clientId);
}
};
@ -239,46 +232,33 @@ 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 {
if (this.activeClients.includes(clientId)) {
this.activeClients = this.activeClients.filter((s) => s !== clientId);
} else {
this.activeClients.push(clientId);
}
this.activeClients.push(clientId);
}
};
editClient = (clientId: string) => {
this.setInfoDialogVisible(false);
this.setPreviewDialogVisible(false);
//@ts-ignore
window?.DocSpace?.navigate(
`/portal-settings/developer-tools/oauth/${clientId}`
`/portal-settings/developer-tools/oauth/${clientId}`,
);
};
fetchClient = async (clientId: string) => {
try {
const client = await getClient(clientId);
return client;
} catch (e: unknown) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
fetchClients = async () => {
try {
this.setClientsIsLoading(true);
const clientList: IClientListProps = await getClientList(0, PAGE_LIMIT);
runInAction(() => {
this.clients = [...this.clients, ...clientList.content];
this.clients = [...clientList.content];
this.selection = [];
this.currentPage = clientList.page;
this.nextPage = clientList.next || null;
if (clientList.next) {
this.itemCount = clientList.content.length + 2;
} else {
@ -289,7 +269,6 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
@ -303,7 +282,6 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
@ -320,7 +298,7 @@ class OAuthStore implements OAuthStoreProps {
const clientList: IClientListProps = await getClientList(
this.nextPage || page,
PAGE_LIMIT
PAGE_LIMIT,
);
runInAction(() => {
@ -350,7 +328,6 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
@ -383,7 +360,6 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
@ -401,21 +377,19 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
regenerateSecret = async (clientId: string) => {
try {
const { client_secret } = await regenerateSecret(clientId);
const { client_secret: clientSecret } = await regenerateSecret(clientId);
this.setClientSecret(client_secret);
this.setClientSecret(clientSecret);
return client_secret;
return clientSecret;
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
@ -432,7 +406,7 @@ class OAuthStore implements OAuthStoreProps {
runInAction(() => {
this.clients = this.clients.filter(
(c) => !clientsId.includes(c.clientId)
(c) => !clientsId.includes(c.clientId),
);
});
@ -440,21 +414,6 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
fetchScope = async (name: string) => {
try {
const scope = await getScope(name);
return scope;
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
return {} as IScope;
}
};
@ -466,7 +425,6 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
@ -483,7 +441,7 @@ class OAuthStore implements OAuthStoreProps {
runInAction(() => {
this.consents = this.consents.filter(
(c) => !clientsId.includes(c.clientId)
(c) => !clientsId.includes(c.clientId),
);
});
@ -491,15 +449,14 @@ class OAuthStore implements OAuthStoreProps {
} catch (e) {
const err = e as TData;
toastr.error(err);
console.log(e);
}
};
getContextMenuItems = (
t: any,
t: TTranslation,
item: IClientProps,
isInfo?: boolean,
isSettings: boolean = true
isSettings: boolean = true,
) => {
const { clientId } = item;
@ -558,7 +515,7 @@ class OAuthStore implements OAuthStoreProps {
];
if (!isSettings) {
const items: any = [];
const items: ContextMenuModel[] = [];
if (!isGroupContext) {
items.push(openOption);
@ -626,7 +583,7 @@ class OAuthStore implements OAuthStoreProps {
this.setActiveClient("");
this.setSelection("");
//TODO OAuth, show toast
// TODO OAuth, show toast
}
};

View File

@ -1,5 +1,5 @@
{
"date": "2024523_191527",
"date": "2024526_13116",
"checksums": {
"api.js": "0efbae3383bf6c6b6f26d573eee164d2",
"api.poly.js": "2a2ac2c0e4a7007b61d2d1ff7b00a22e",

View File

@ -23,6 +23,25 @@ export interface INoAuthClientProps {
policyUrl?: string;
termsUrl?: string;
scopes?: string[];
clientId?: undefined;
clientSecret?: undefined;
description?: undefined;
authenticationMethods?: undefined;
tenant?: undefined;
redirectUris?: undefined;
logoutRedirectUri?: undefined;
enabled?: undefined;
invalidated?: undefined;
allowedOrigins?: undefined;
createdOn?: undefined;
modifiedOn?: undefined;
createdBy?: undefined;
modifiedBy?: undefined;
creatorAvatar?: undefined;
creatorDisplayName?: undefined;
}
export interface IClientProps {
@ -42,7 +61,6 @@ export interface IClientProps {
scopes: string[];
websiteUrl: string;
allowedOrigins: string[];
createdOn?: Date;
modifiedOn?: Date;
createdBy?: string;