Client:Pages:OAuth2: fix create and edit form after merge develop
This commit is contained in:
parent
33bc642d0d
commit
e7c97f0806
@ -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"
|
||||
|
@ -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 />
|
||||
)}
|
||||
|
@ -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 {
|
||||
|
@ -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 />;
|
||||
};
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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 };
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,4 +2,9 @@ import styled from "styled-components";
|
||||
|
||||
export const OAuthContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
.ec-subheading {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
@ -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")}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
|
||||
export interface EmptyScreenProps {
|
||||
t: TTranslation;
|
||||
}
|
||||
|
@ -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} />}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"date": "2024523_191527",
|
||||
"date": "2024526_13116",
|
||||
"checksums": {
|
||||
"api.js": "0efbae3383bf6c6b6f26d573eee164d2",
|
||||
"api.poly.js": "2a2ac2c0e4a7007b61d2d1ff7b00a22e",
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user