Compare commits
209 Commits
master
...
feature/re
Author | SHA1 | Date | |
---|---|---|---|
41a6083a4b | |||
fc3593eab1 | |||
925cc648f5 | |||
8fd8aed172 | |||
9dd2820abc | |||
f1e6f79704 | |||
320d882d86 | |||
99e265ebee | |||
6c727811a0 | |||
b354c305cf | |||
ae294f7e64 | |||
dd435561a5 | |||
a966d2e948 | |||
5f712f87f3 | |||
2ffa095303 | |||
e2205d9387 | |||
8f4ddba367 | |||
f612ba9edf | |||
572e16a7f2 | |||
dea872e497 | |||
11b5de43c8 | |||
def151fe66 | |||
341110b486 | |||
10b2e65d7f | |||
b2f47a0125 | |||
125590044e | |||
ab24a40a0e | |||
d7914ad024 | |||
ffe98ad175 | |||
0fb2b762c0 | |||
ec907c512d | |||
f16ba9f57e | |||
252eb14be5 | |||
6145817cb8 | |||
b7fbb88185 | |||
9419551e8c | |||
05c2f12d44 | |||
58c6248d1f | |||
680437524f | |||
57d60d1798 | |||
a3af4a49cc | |||
9c986d3bc7 | |||
fb3a0c51af | |||
5e70e6fee5 | |||
996c79d617 | |||
da29dab3bf | |||
d78e24f4bf | |||
9594256e2f | |||
626f38f283 | |||
02b4294e63 | |||
e7c97f0806 | |||
33bc642d0d | |||
3bb3c7d5ea | |||
ac47e07815 | |||
aa772acb03 | |||
9573816860 | |||
b7e9886d70 | |||
9e981c09da | |||
71e1ed6a77 | |||
8b828f9e2a | |||
ea5dd21226 | |||
6d0dd00052 | |||
c346033713 | |||
740996ca7e | |||
f129e8f1dc | |||
16ada7a735 | |||
a855f7b345 | |||
e9602c6c43 | |||
d5fd4a7868 | |||
55d7b5e500 | |||
fbd5335167 | |||
06da361ba3 | |||
82e0c8e3e9 | |||
edc3636ba0 | |||
84bc76263f | |||
8ed65d92d8 | |||
80ebe7c16a | |||
86ed0acd04 | |||
20c3d69d42 | |||
8b9c3af330 | |||
20073c7158 | |||
71c42ce188 | |||
55d7dbcd64 | |||
2fdc2d84ff | |||
ea9073c4e0 | |||
611ba72c9a | |||
8b27e2f47a | |||
ec66aae56a | |||
fa4a1c5213 | |||
f6f8f67757 | |||
31982b1cfd | |||
4ca49ed173 | |||
0d3073375f | |||
7031ade6dd | |||
7cbcdb8180 | |||
de3d152a9c | |||
722ef797cb | |||
951de70db7 | |||
fea17b461c | |||
0c1b4420a7 | |||
50bcf1d577 | |||
1f6679f29a | |||
4a09f4c562 | |||
a525c677ef | |||
2cf00f3e1d | |||
961b47feb7 | |||
312140a0dc | |||
008a2b07ad | |||
7c1387ab53 | |||
9fab8360ce | |||
f3ce27d404 | |||
f9bf6e08d0 | |||
6e3b5d0338 | |||
61ae9c1a72 | |||
377f543baf | |||
8391dad883 | |||
c00351f0f6 | |||
711e799b05 | |||
a80c0318ab | |||
ae5b4d4ed8 | |||
9dd8624bbb | |||
5b5011ce8b | |||
0a4cb47576 | |||
139c1f2cb3 | |||
ab1738172c | |||
3d766424e0 | |||
20b79e4833 | |||
571031397c | |||
280400f0a2 | |||
6c78635489 | |||
dd964bdbf9 | |||
70ab743bf0 | |||
045452631e | |||
ebe91b23b2 | |||
a02fac57f0 | |||
01bb18a6a3 | |||
6ce021dd3a | |||
97e3c2769d | |||
c1c9b7de02 | |||
f0c94941e8 | |||
e691e88771 | |||
8c8df8af5a | |||
9067d28459 | |||
85c59b1075 | |||
b179efeeae | |||
76fdf6e9b6 | |||
7902e9824d | |||
147779cb13 | |||
fe29d4f897 | |||
9f576f112c | |||
b593aa13f3 | |||
8318dbb8f5 | |||
86f17497a8 | |||
348383e2fb | |||
a4f555b853 | |||
51cc80ecfa | |||
b5c55da859 | |||
ed4b027bcc | |||
f631a208f7 | |||
380bec5048 | |||
16fca0a7c8 | |||
5d30cdca13 | |||
8dbfda6f2b | |||
f5c2bd64db | |||
bba1e34a09 | |||
86bded9492 | |||
8dd604dc53 | |||
ad047bc372 | |||
e02510be1d | |||
bb5b8cccdd | |||
ba23f9bb7b | |||
84afb50b66 | |||
aa6545bfe1 | |||
4b888c6640 | |||
a34bedef1e | |||
f6dca051ac | |||
ca73c5b0c4 | |||
807215a0cb | |||
2381fcf68f | |||
8ee516850b | |||
0f9d61b9f0 | |||
bbce172bba | |||
5f8718c78d | |||
76e439bad3 | |||
d8a535432d | |||
6956adc231 | |||
310f2d5dc5 | |||
61c920555b | |||
8b0bca3a44 | |||
74e94c0a10 | |||
dc1274fb88 | |||
3b9c56c5f4 | |||
a56a46236c | |||
17f6195d8c | |||
31bf677340 | |||
8e751923d9 | |||
ebdffb55ea | |||
ac496b82a2 | |||
6d6116c5ce | |||
11c322f03b | |||
2a36d19707 | |||
4a7f1710de | |||
a4d69eebe8 | |||
e173c5932d | |||
fbaace1941 | |||
a0b9e56097 | |||
06c63bdb6f | |||
d5b009554e | |||
eefb4b5e2e |
45
.vscode/tasks.json
vendored
45
.vscode/tasks.json
vendored
@ -62,6 +62,21 @@
|
||||
"close": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend | build SAAS + dnsmasq + identity",
|
||||
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -s -d -i",
|
||||
"type": "shell",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"focus": true,
|
||||
"close": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend | rebuild SAAS + dnsmasq",
|
||||
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -s -d -f",
|
||||
@ -92,6 +107,36 @@
|
||||
"close": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend | build EE + identity",
|
||||
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -i",
|
||||
"type": "shell",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"focus": true,
|
||||
"close": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend | build EE + dnsmasq + identity",
|
||||
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/build.backend.docker.py -d -i",
|
||||
"type": "shell",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"focus": true,
|
||||
"close": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend | clear",
|
||||
"command": "cd ${workspaceFolder}/../ ; ${command:python.interpreterPath} buildtools/clear.backend.docker.py",
|
||||
|
@ -67,6 +67,11 @@
|
||||
"task": "Backend | build SAAS + dnsmasq",
|
||||
"tooltip": "🛠️ Start the \"backend docker build SAAS + dnsmasq\" task",
|
||||
},
|
||||
{
|
||||
"label": "Docker : Build-SAAS + dnsmasq + identity",
|
||||
"task": "Backend | build SAAS + dnsmasq + identity",
|
||||
"tooltip": "🛠️ Start the \"backend docker build SAAS + dnsmasq + identity\" task",
|
||||
},
|
||||
{
|
||||
"label": "Docker : ReBuild-SAAS + dnsmasq",
|
||||
"task": "Backend | rebuild SAAS + dnsmasq",
|
||||
@ -77,6 +82,16 @@
|
||||
"task": "Backend | build EE + dnsmasq",
|
||||
"tooltip": "🛠️ Start the \"backend docker build EE + dnsmasq\" task",
|
||||
},
|
||||
{
|
||||
"label": "Docker : Build-EE + identity",
|
||||
"task": "Backend | build EE + identity",
|
||||
"tooltip": "🛠️ Start the \"backend docker build EE + identity\" task",
|
||||
},
|
||||
{
|
||||
"label": "Docker : Build-EE + dnsmasq + identity",
|
||||
"task": "Backend | build EE + dnsmasq + identity",
|
||||
"tooltip": "🛠️ Start the \"backend docker build EE + dnsmasq + identity\" task",
|
||||
},
|
||||
{
|
||||
"label": "Docker : Clear",
|
||||
"task": "Backend | clear",
|
||||
|
@ -48,6 +48,7 @@
|
||||
"@uiw/codemirror-theme-github": "^4.21.25",
|
||||
"@uiw/react-codemirror": "^4.21.24",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-resize-detector": "^1.2.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"firebase": "^10.8.0",
|
||||
@ -73,6 +74,7 @@
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/eslint": "^8.44.7",
|
||||
"@types/he": "^1.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
|
69
packages/client/public/locales/en/OAuth.json
Normal file
69
packages/client/public/locales/en/OAuth.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"Access": "Access",
|
||||
"AccessGranted": "Access granted",
|
||||
"AppIcon": "App icon",
|
||||
"AllowedOrigins": "Allowed origins",
|
||||
"AllowedOriginsHelpButton": "URLs added here are used to improve the OAuth redirect security.",
|
||||
"AllowPKCE": "Allow public client (PKCE)",
|
||||
"AllowPKCEHelpButton": "PKCE is not a form of client authentication, and PKCE is not a replacement for a client secret or another client authentication type. PKCE is recommended even if a client is using a client secret or another form of client authentication like private_key_jwt.<br/> <strong>Note</strong>: Since PKCE is not a replacement for client authentication, it does not allow treating a public client as confidential one.",
|
||||
"AppName": "App name",
|
||||
"Apps": "Applications",
|
||||
"AuthButton": "Auth button",
|
||||
"AuthorizedApps": "Authorized apps",
|
||||
"AuthorizeLink": "Authorize link",
|
||||
"AuthenticationMethod": "Authentication method",
|
||||
"Client": "Client",
|
||||
"ClientCopy": "Client id successfully copied to clipboard",
|
||||
"Creator": "Creator",
|
||||
"ClientHelpButton": "Credentials for using OAth 2.0 as your Authentication type.<br/> <strong>Note</strong>: Any enterprise admin who knows the app's client ID will be able to retrieve information about the app including app name, authentication type, app scopes and redirect URI.",
|
||||
"CodeVerifier": "Code verifier",
|
||||
"DeleteHeader": "Delete application",
|
||||
"DeleteDescription": "If you delete this application, all active consents and authorization will be revoked. If the user tries to open the consent screen for this app, an error will be thrown in the document space and the user will be redirected to the specified redirect URL.",
|
||||
"DisableApplication": "Disable application",
|
||||
"DisableApplicationDescription": "If you disable this application, all active consents and authorization will be disabled. If necessary, you can later enable the disabled application.",
|
||||
"EditApp": "Edit application",
|
||||
"EnterDescription": "Enter description",
|
||||
"ErrorName": "Minimal name length:",
|
||||
"ErrorWrongURL": "URL not valid, example",
|
||||
"EnterURL": "Enter URL",
|
||||
"IconDescription": "JPG, PNG or SVG, 32x32",
|
||||
"ID": "ID",
|
||||
"LastModified": "Last modified",
|
||||
"NewApp": "New application",
|
||||
"NoAuthorizedApps": "No authorized apps",
|
||||
"NoOAuthAppHeader": "No OAuth applications",
|
||||
"OAuth": "OAuth 2.0",
|
||||
"OAuthAppDescription": "OAuth applications are used to access the ONLYOFFICE DocSpace API for authorization and further actions such as accessing files, etc.",
|
||||
"OAuthHeaderBlock": "OAuth urls",
|
||||
"ProfileDescription": "Here you can check the apps info to which you have granted the auth access, and revoke consent if needed.",
|
||||
"PrivacyPolicy": "Privacy policy",
|
||||
"PrivacyPolicyURL": "Privacy policy URL",
|
||||
"PrivacyPolicyURLHelpButton": "Provide a URL link to your Privacy Policy that must comply with applicable laws and regulations and that make clear how you collect, use, share, retain and otherwise process personal information.",
|
||||
"Read": "Read",
|
||||
"RedirectsURLS": "Redirects URLS",
|
||||
"RedirectsURLSHelpButton": "After a user successfully authorizes an application, the authorization server will redirect the user back to the application with sensitive information.",
|
||||
"RegisterNewApp": "Register a new application",
|
||||
"Reset": "Reset",
|
||||
"ResetHeader": "Reset client secret",
|
||||
"ResetDescription": "If you reset client secret, all active consents and authorization will be revoked. For apply next consent need use new client secret. Note that all users will again be required to complete the consent screen.",
|
||||
"Revoke": "Revoke",
|
||||
"RevokeConsent": "Revoke consent",
|
||||
"RevokeConsentDescription": "Once you revoke the consent to use the ONLYOFFICE DocSpace auth data in the service {{name}}, ONLYOFFICE DocSpace will automatically stop logging into {{name}}. Your account in {{name}} will not be deleted.",
|
||||
"RevokeConsentDescriptionGroup": "Once you revoke the consent to use the ONLYOFFICE DocSpace auth data in the services, ONLYOFFICE DocSpace will automatically stop logging. Your accounts will not be deleted.",
|
||||
"RevokeConsentLogin": "If you want to renew an automatic login into {{name}} using ONLYOFFICE DocSpace, you will be asked to grant access to your DocSpace account data.",
|
||||
"RevokeConsentLoginGroup": "If you want to renew an automatic login using ONLYOFFICE DocSpace, you will be asked to grant access to your DocSpace account data.",
|
||||
"Secret": "Secret",
|
||||
"SecretCopy": "Client secret successfully copied to clipboard",
|
||||
"SelectNewImage": "Select new image",
|
||||
"Scopes": "Scopes",
|
||||
"ScopesHeader": "Access scopes",
|
||||
"ScopesHelp": "Scopes are used to limit your app's access to all user-related data, and they'll let you specify exactly what kind of access you need.",
|
||||
"SignIn": "Sign in with DocSpace",
|
||||
"SupportAndLegalInfo": "Support & Legal info",
|
||||
"TermsOfService": "Terms of Service",
|
||||
"TermsOfServiceURL": "Terms of Service URL",
|
||||
"TermsOfServiceURLHelpButton": "Terms and conditions that users must comply with when using this application.",
|
||||
"ThisRequiredField": "This is a required field",
|
||||
"WebsiteUrl": "Website URL",
|
||||
"Write": "Write"
|
||||
}
|
@ -31,7 +31,7 @@ import { DeviceType } from "@docspace/shared/enums";
|
||||
import { isTablet, isMobile, Context } from "@docspace/shared/utils";
|
||||
import { isMobile as isMobileDevice } from "react-device-detect";
|
||||
|
||||
type DeviceUnionType = (typeof DeviceType)[keyof typeof DeviceType];
|
||||
export type DeviceUnionType = (typeof DeviceType)[keyof typeof DeviceType];
|
||||
|
||||
type useViewEffectProps = {
|
||||
view: string;
|
||||
|
@ -46,6 +46,8 @@ const PrivateRouteWrapper = ({
|
||||
restricted,
|
||||
withCollaborator,
|
||||
withManager,
|
||||
identityServerEnabled,
|
||||
limitedAccessSpace,
|
||||
baseDomain,
|
||||
}: Partial<PrivateRouteProps>) => {
|
||||
return (
|
||||
@ -65,6 +67,8 @@ const PrivateRouteWrapper = ({
|
||||
withCollaborator={withCollaborator}
|
||||
isPortalDeactivate={isPortalDeactivate!}
|
||||
enablePortalRename={enablePortalRename!}
|
||||
identityServerEnabled={identityServerEnabled}
|
||||
limitedAccessSpace={limitedAccessSpace ?? null}
|
||||
baseDomain={baseDomain}
|
||||
>
|
||||
{children}
|
||||
@ -82,7 +86,10 @@ export default inject<TStore>(
|
||||
isLogout,
|
||||
isCommunity,
|
||||
isEnterprise,
|
||||
capabilities,
|
||||
} = authStore;
|
||||
|
||||
const identityServerEnabled = capabilities?.identityServerEnabled;
|
||||
const { isNotPaidPeriod } = currentTariffStatusStore;
|
||||
const { user } = userStore;
|
||||
|
||||
@ -91,6 +98,7 @@ export default inject<TStore>(
|
||||
tenantStatus,
|
||||
isPortalDeactivate,
|
||||
enablePortalRename,
|
||||
limitedAccessSpace,
|
||||
baseDomain,
|
||||
} = settingsStore;
|
||||
|
||||
@ -107,6 +115,8 @@ export default inject<TStore>(
|
||||
isLogout,
|
||||
isEnterprise,
|
||||
enablePortalRename,
|
||||
identityServerEnabled,
|
||||
limitedAccessSpace,
|
||||
baseDomain,
|
||||
};
|
||||
},
|
||||
|
@ -27,8 +27,9 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Loader } from "@docspace/shared/components/loader";
|
||||
import Section from "@docspace/shared/components/section";
|
||||
import { getCookie, deleteCookie } from "@docspace/shared/utils/cookie";
|
||||
import { loginWithConfirmKey } from "@docspace/shared/api/user";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useSearchParams, useLocation } from "react-router-dom";
|
||||
import { combineUrl } from "@docspace/shared/utils/combineUrl";
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
import { frameCallEvent } from "@docspace/shared/utils/common";
|
||||
@ -37,6 +38,7 @@ const Auth = (props) => {
|
||||
//console.log("Auth render");
|
||||
const { linkData } = props;
|
||||
let [searchParams, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
loginWithConfirmKey({
|
||||
ConfirmData: {
|
||||
@ -50,6 +52,22 @@ const Auth = (props) => {
|
||||
|
||||
const url = searchParams.get("referenceUrl");
|
||||
|
||||
const redirectUrl = getCookie("x-redirect-authorization-uri");
|
||||
|
||||
deleteCookie("x-redirect-authorization-uri");
|
||||
|
||||
if (redirectUrl) {
|
||||
window.location.replace(redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url && url.includes("oauth2")) {
|
||||
const newUrl = location.search.split("referenceUrl=")[1];
|
||||
|
||||
window.location.replace(newUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
new URL(url);
|
||||
|
@ -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;
|
||||
@ -157,7 +157,7 @@ const HeaderContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
export const StyledContainer = styled.div`
|
||||
.group-button-menu-container {
|
||||
${(props) =>
|
||||
props.viewAs === "table"
|
||||
|
@ -31,13 +31,14 @@ import { SectionHeaderContent, SectionPagingContent } from "./Section";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import Section from "@docspace/shared/components/section";
|
||||
import withLoading from "SRC_DIR/HOCs/withLoading";
|
||||
import ArticleWrapper from "SRC_DIR/components/ArticleWrapper";
|
||||
|
||||
import SectionWrapper from "SRC_DIR/components/Section";
|
||||
|
||||
import { useParams } from "react-router-dom";
|
||||
import HistoryHeader from "../categories/developer-tools/Webhooks/WebhookHistory/sub-components/HistoryHeader";
|
||||
import DetailsNavigationHeader from "../categories/developer-tools/Webhooks/WebhookEventDetails/sub-components/DetailsNavigationHeader";
|
||||
import ArticleWrapper from "SRC_DIR/components/ArticleWrapper";
|
||||
import OAuthSectionHeader from "../categories/developer-tools/OAuth/OAuthSectionHeader";
|
||||
|
||||
const ArticleSettings = React.memo(({ showArticleLoader, needPageReload }) => {
|
||||
const onLogoClickAction = () => {
|
||||
@ -88,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 (
|
||||
@ -107,6 +110,9 @@ const Layout = ({
|
||||
<HistoryHeader />
|
||||
) : currentPath === webhookDetailsPath ? (
|
||||
<DetailsNavigationHeader />
|
||||
) : currentPath === oauthCreatePath ||
|
||||
currentPath === oauthEditPath ? (
|
||||
<OAuthSectionHeader />
|
||||
) : (
|
||||
<SectionHeaderContent />
|
||||
)}
|
||||
|
@ -171,7 +171,7 @@ export const Frame = styled(Box)`
|
||||
position: relative;
|
||||
|
||||
border-radius: 6px;
|
||||
border: 1px solid ${(props) => props.theme.sdkPresets.borderColor};
|
||||
border: 1px solid ${(props) => props.theme.sdkPresets?.borderColor};
|
||||
|
||||
width: calc(${(props) => (props.width ? props.width : "100%")} + 2px);
|
||||
height: calc(${(props) => (props.height ? props.height : "100%")} + 2px);
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
|
||||
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
|
||||
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
export interface OAuthProps {
|
||||
viewAs: ViewAsType;
|
||||
setViewAs: (viewAs: string) => void;
|
||||
|
||||
clientList: IClientProps[];
|
||||
isEmptyClientList: boolean;
|
||||
fetchClients: () => Promise<void>;
|
||||
fetchScopes: () => Promise<void>;
|
||||
|
||||
currentDeviceType: DeviceUnionType;
|
||||
|
||||
infoDialogVisible?: boolean;
|
||||
previewDialogVisible?: boolean;
|
||||
disableDialogVisible?: boolean;
|
||||
deleteDialogVisible?: boolean;
|
||||
isInit: boolean;
|
||||
setIsInit: (value: boolean) => void;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
|
||||
|
||||
import ClientForm from "../sub-components/ClientForm";
|
||||
|
||||
const OAuthCreatePage = () => {
|
||||
const { t } = useTranslation(["OAuth"]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDocumentTitle(t("OAuth"));
|
||||
}, [t]);
|
||||
|
||||
return <ClientForm />;
|
||||
};
|
||||
|
||||
export default OAuthCreatePage;
|
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
|
||||
|
||||
import ClientForm from "../sub-components/ClientForm";
|
||||
|
||||
const OAuthEditPage = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
const { t } = useTranslation(["OAuth"]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDocumentTitle(t("OAuth"));
|
||||
}, [t]);
|
||||
|
||||
return <ClientForm id={id} />;
|
||||
};
|
||||
|
||||
export default OAuthEditPage;
|
@ -0,0 +1,49 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
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 {
|
||||
StyledContainer,
|
||||
HeaderContainer,
|
||||
} from "../../../../Layout/Section/Header";
|
||||
|
||||
const OAuthSectionHeader = ({ isEdit }: { isEdit: boolean }) => {
|
||||
const { t, ready } = useTranslation(["OAuth"]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onBack = () => {
|
||||
navigate("/portal-settings/developer-tools/oauth");
|
||||
};
|
||||
|
||||
if (!ready) return <LoaderSectionHeader />;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthSectionHeader;
|
@ -0,0 +1,10 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const OAuthContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
.ec-subheading {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
@ -0,0 +1,148 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
|
||||
|
||||
import useViewEffect from "SRC_DIR/Hooks/useViewEffect";
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
import { setDocumentTitle } from "SRC_DIR/helpers/utils";
|
||||
|
||||
import { OAuthContainer } from "./StyledOAuth";
|
||||
import { OAuthProps } from "./OAuth.types";
|
||||
|
||||
import InfoDialog from "./sub-components/InfoDialog";
|
||||
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 OAuthEmptyScreen from "./sub-components/EmptyScreen";
|
||||
import List from "./sub-components/List";
|
||||
|
||||
const MIN_LOADER_TIME = 500;
|
||||
|
||||
const OAuth = ({
|
||||
clientList,
|
||||
viewAs,
|
||||
isEmptyClientList,
|
||||
setViewAs,
|
||||
fetchClients,
|
||||
fetchScopes,
|
||||
currentDeviceType,
|
||||
infoDialogVisible,
|
||||
previewDialogVisible,
|
||||
isInit,
|
||||
setIsInit,
|
||||
disableDialogVisible,
|
||||
deleteDialogVisible,
|
||||
}: OAuthProps) => {
|
||||
const { t } = useTranslation(["OAuth"]);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const startLoadingRef = React.useRef<null | Date>(null);
|
||||
|
||||
const getData = React.useCallback(async () => {
|
||||
if (isInit) return;
|
||||
const actions = [];
|
||||
|
||||
actions.push(fetchScopes(), fetchClients());
|
||||
|
||||
await Promise.all(actions);
|
||||
|
||||
if (startLoadingRef.current) {
|
||||
const currentDate = new Date();
|
||||
|
||||
const ms = Math.abs(
|
||||
startLoadingRef.current.getTime() - currentDate.getTime(),
|
||||
);
|
||||
|
||||
if (ms < MIN_LOADER_TIME)
|
||||
return setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsInit(true);
|
||||
}, MIN_LOADER_TIME - ms);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setIsInit(true);
|
||||
}, [fetchClients, fetchScopes, isInit, setIsInit]);
|
||||
|
||||
useViewEffect({
|
||||
view: viewAs,
|
||||
setView: setViewAs,
|
||||
currentDeviceType,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInit) return setIsLoading(false);
|
||||
startLoadingRef.current = new Date();
|
||||
getData();
|
||||
}, [getData, setIsInit, isInit]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDocumentTitle(t("OAuth"));
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<OAuthContainer>
|
||||
{isLoading ? (
|
||||
<OAuthLoader viewAs={viewAs} currentDeviceType={currentDeviceType} />
|
||||
) : isEmptyClientList ? (
|
||||
<OAuthEmptyScreen />
|
||||
) : (
|
||||
<List
|
||||
clients={clientList}
|
||||
viewAs={viewAs}
|
||||
currentDeviceType={currentDeviceType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{infoDialogVisible && <InfoDialog visible={infoDialogVisible} />}
|
||||
{disableDialogVisible && <DisableDialog />}
|
||||
{previewDialogVisible && <PreviewDialog visible={previewDialogVisible} />}
|
||||
{deleteDialogVisible && <DeleteDialog />}
|
||||
</OAuthContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({
|
||||
oauthStore,
|
||||
settingsStore,
|
||||
}: {
|
||||
oauthStore: OAuthStoreProps;
|
||||
settingsStore: SettingsStore;
|
||||
}) => {
|
||||
const { currentDeviceType } = settingsStore;
|
||||
const {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
clientList,
|
||||
isEmptyClientList,
|
||||
fetchClients,
|
||||
fetchScopes,
|
||||
infoDialogVisible,
|
||||
previewDialogVisible,
|
||||
isInit,
|
||||
setIsInit,
|
||||
disableDialogVisible,
|
||||
deleteDialogVisible,
|
||||
} = oauthStore;
|
||||
return {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
clientList,
|
||||
isEmptyClientList,
|
||||
fetchClients,
|
||||
currentDeviceType,
|
||||
infoDialogVisible,
|
||||
previewDialogVisible,
|
||||
fetchScopes,
|
||||
isInit,
|
||||
setIsInit,
|
||||
disableDialogVisible,
|
||||
deleteDialogVisible,
|
||||
};
|
||||
},
|
||||
)(observer(OAuth));
|
@ -0,0 +1,315 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { mobile } from "@docspace/shared/utils/device";
|
||||
import { Base } from "@docspace/shared/themes";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 24px;
|
||||
|
||||
.loader {
|
||||
rect {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.scope-name-loader {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.scope-desc-loader {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBlock = styled.div`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.icon-field {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeaderRow = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
height: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputBlock = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
gap: 16px;
|
||||
|
||||
@media ${mobile} {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInputGroup = styled.div`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pkce {
|
||||
margin-top: 4px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0px;
|
||||
}
|
||||
|
||||
.public_client {
|
||||
margin-top: 4px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
|
||||
.client-logo {
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.oauth.clientForm.descriptionColor};
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: ${(props) => props.theme.oauth.clientForm.descriptionColor};
|
||||
}
|
||||
|
||||
.input-block-with-button {
|
||||
.field-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledInputGroup.defaultProps = { theme: Base };
|
||||
|
||||
const StyledInputRow = styled.div`
|
||||
width: 100%;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
input {
|
||||
user-select: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledChipsContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
const StyledScopesContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content max-content;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 16px 0;
|
||||
|
||||
.header {
|
||||
padding-bottom: 8px;
|
||||
|
||||
padding-right: 24px;
|
||||
margin-right: -12px;
|
||||
|
||||
border-bottom: ${(props) => props.theme.oauth.clientForm.headerBorder};
|
||||
}
|
||||
|
||||
.header-last {
|
||||
margin-right: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.checkbox-read {
|
||||
margin-right: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
StyledScopesContainer.defaultProps = { theme: Base };
|
||||
|
||||
const StyledScopesName = styled.div`
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
.scope-name {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.scope-desc {
|
||||
color: ${(props) => props.theme.oauth.clientForm.scopeDesc};
|
||||
}
|
||||
`;
|
||||
|
||||
StyledScopesName.defaultProps = { theme: Base };
|
||||
|
||||
const StyledScopesCheckbox = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
|
||||
.checkbox {
|
||||
margin-right: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
width: fit-content;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
@media ${mobile} {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
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: ${(props) => props.theme.oauth.clientForm.headerBorder};
|
||||
|
||||
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,
|
||||
StyledHeaderRow,
|
||||
StyledInputBlock,
|
||||
StyledInputGroup,
|
||||
StyledInputRow,
|
||||
StyledChipsContainer,
|
||||
StyledScopesContainer,
|
||||
StyledScopesName,
|
||||
StyledScopesCheckbox,
|
||||
StyledButtonContainer,
|
||||
StyledInputAddBlock,
|
||||
StyledCheckboxGroup,
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import {
|
||||
IClientProps,
|
||||
IClientReqDTO,
|
||||
TScope,
|
||||
} from "@docspace/shared/utils/oauth/types";
|
||||
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;
|
||||
placeholder: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
|
||||
isReadOnly?: boolean;
|
||||
isSecret?: boolean;
|
||||
withCopy?: boolean;
|
||||
|
||||
withButton?: boolean;
|
||||
buttonLabel?: string;
|
||||
onClickButton?: () => void;
|
||||
|
||||
multiplyInput?: boolean;
|
||||
}
|
||||
|
||||
export interface CheckboxProps {
|
||||
isChecked: boolean;
|
||||
onChange: () => void;
|
||||
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface BlockProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ClientFormProps {
|
||||
id?: string;
|
||||
client?: IClientProps;
|
||||
|
||||
scopeList?: TScope[];
|
||||
|
||||
fetchScopes?: () => Promise<void>;
|
||||
|
||||
saveClient?: (client: IClientReqDTO) => Promise<void>;
|
||||
updateClient?: (clientId: string, client: IClientReqDTO) => Promise<void>;
|
||||
|
||||
resetDialogVisible?: boolean;
|
||||
setResetDialogVisible?: (value: boolean) => void;
|
||||
|
||||
currentDeviceType?: DeviceUnionType;
|
||||
|
||||
setClientSecretProps?: (value: string) => void;
|
||||
clientSecretProps?: string;
|
||||
}
|
||||
|
||||
export interface ClientStore {
|
||||
settingsStore: SettingsStore;
|
||||
oauthStore: OAuthStoreProps;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
import React from "react";
|
||||
|
||||
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
|
||||
|
||||
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
|
||||
|
||||
import {
|
||||
StyledBlock,
|
||||
StyledButtonContainer,
|
||||
StyledCheckboxGroup,
|
||||
StyledContainer,
|
||||
StyledHeaderRow,
|
||||
StyledInputBlock,
|
||||
StyledInputGroup,
|
||||
StyledInputRow,
|
||||
StyledScopesCheckbox,
|
||||
StyledScopesContainer,
|
||||
StyledScopesName,
|
||||
} from "./ClientForm.styled";
|
||||
|
||||
const HelpButtonSkeleton = () => {
|
||||
return <RectangleSkeleton width="12px" height="12px" />;
|
||||
};
|
||||
|
||||
const CheckboxSkeleton = ({ className }: { className?: string }) => {
|
||||
return <RectangleSkeleton className={className} width="16px" height="16px" />;
|
||||
};
|
||||
|
||||
const ClientFormLoader = ({
|
||||
currentDeviceType,
|
||||
isEdit,
|
||||
}: {
|
||||
currentDeviceType?: DeviceUnionType;
|
||||
isEdit: boolean;
|
||||
}) => {
|
||||
const buttonHeight = currentDeviceType !== "desktop" ? "40px" : "32px";
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledBlock>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="78px" height="22px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputBlock>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="65px" height="20px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton width="100%" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="80px" height="20px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton width="100%" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
<StyledInputGroup>
|
||||
<div className="label">
|
||||
<RectangleSkeleton width="60px" height="20px" />
|
||||
</div>
|
||||
<div className="select">
|
||||
<RectangleSkeleton width="32px" height="32px" />
|
||||
<RectangleSkeleton width="32px" height="32px" />
|
||||
<RectangleSkeleton width="109px" height="20px" />
|
||||
</div>
|
||||
<RectangleSkeleton width="130px" height="16px" />
|
||||
</StyledInputGroup>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="75px" height="20px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<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="22px" />
|
||||
<HelpButtonSkeleton />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputBlock>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="96px" height="20px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton width="100%" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="60px" height="20px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton
|
||||
className="loader"
|
||||
width="calc(100% - 91px)"
|
||||
height="32px"
|
||||
/>
|
||||
<RectangleSkeleton width="91px" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
)}
|
||||
<StyledBlock>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="96px" height="22px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputBlock>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="87px" height="20px" />
|
||||
<HelpButtonSkeleton />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton
|
||||
className="loader"
|
||||
width="calc(100% - 40px)"
|
||||
height="32px"
|
||||
/>
|
||||
<RectangleSkeleton width="32px" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="96px" height="20px" />
|
||||
<HelpButtonSkeleton />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton
|
||||
className="loader"
|
||||
width="calc(100% - 40px)"
|
||||
height="32px"
|
||||
/>
|
||||
<RectangleSkeleton width="32px" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
<StyledScopesContainer>
|
||||
<StyledHeaderRow className="header">
|
||||
<RectangleSkeleton width="111px" height="22px" />
|
||||
<HelpButtonSkeleton />
|
||||
</StyledHeaderRow>
|
||||
<RectangleSkeleton className="header" width="34px" height="22px" />
|
||||
<RectangleSkeleton
|
||||
className="header header-last"
|
||||
width="37px"
|
||||
height="22px"
|
||||
/>
|
||||
<StyledScopesName>
|
||||
<RectangleSkeleton
|
||||
className="scope-name-loader"
|
||||
width="98px"
|
||||
height="16px"
|
||||
/>
|
||||
<RectangleSkeleton
|
||||
className="scope-desc-loader"
|
||||
width="200px"
|
||||
height="17px"
|
||||
/>
|
||||
<RectangleSkeleton
|
||||
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"
|
||||
/>
|
||||
<RectangleSkeleton
|
||||
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"
|
||||
/>
|
||||
<RectangleSkeleton
|
||||
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"
|
||||
/>
|
||||
<RectangleSkeleton
|
||||
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="22px" />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputBlock>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="114px" height="20px" />
|
||||
<HelpButtonSkeleton />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton width="100%" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
<StyledInputGroup>
|
||||
<StyledHeaderRow>
|
||||
<RectangleSkeleton width="96px" height="20px" />
|
||||
<HelpButtonSkeleton />
|
||||
</StyledHeaderRow>
|
||||
<StyledInputRow>
|
||||
<RectangleSkeleton width="100%" height="32px" />
|
||||
</StyledInputRow>
|
||||
</StyledInputGroup>
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
<StyledButtonContainer>
|
||||
<RectangleSkeleton
|
||||
width={currentDeviceType === "desktop" ? "86px" : "100%"}
|
||||
height={buttonHeight}
|
||||
/>
|
||||
<RectangleSkeleton
|
||||
width={currentDeviceType === "desktop" ? "86px" : "100%"}
|
||||
height={buttonHeight}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientFormLoader;
|
@ -0,0 +1,257 @@
|
||||
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/types";
|
||||
// import { ToggleButton } from "@docspace/shared/components/toggle-button";
|
||||
// import { Text } from "@docspace/shared/components/text";
|
||||
|
||||
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
|
||||
|
||||
import BlockHeader from "./BlockHeader";
|
||||
import InputGroup from "./InputGroup";
|
||||
import TextAreaGroup from "./TextAreaGroup";
|
||||
import SelectGroup from "./SelectGroup";
|
||||
|
||||
interface BasicBlockProps {
|
||||
t: TTranslation;
|
||||
|
||||
nameValue: string;
|
||||
websiteUrlValue: string;
|
||||
logoValue: string;
|
||||
descriptionValue: string;
|
||||
allowPkce: boolean;
|
||||
// isPublic: boolean;
|
||||
|
||||
changeValue: (
|
||||
name: keyof IClientReqDTO,
|
||||
value: string | boolean,
|
||||
remove?: boolean,
|
||||
) => void;
|
||||
|
||||
isEdit: boolean;
|
||||
errorFields: string[];
|
||||
requiredErrorFields: string[];
|
||||
onBlur: (name: string) => void;
|
||||
}
|
||||
|
||||
function getImageDimensions(
|
||||
image: HTMLImageElement,
|
||||
): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve) => {
|
||||
image.onload = () => {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
resolve({ height, width });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function compressImage(
|
||||
image: HTMLImageElement,
|
||||
scale: number,
|
||||
initialWidth: number,
|
||||
initialHeight: number,
|
||||
): Promise<Blob | undefined | null> {
|
||||
return new Promise((resolve) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
canvas.width = scale * initialWidth;
|
||||
canvas.height = scale * initialHeight;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
}, "image/png");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const BasicBlock = ({
|
||||
t,
|
||||
nameValue,
|
||||
websiteUrlValue,
|
||||
logoValue,
|
||||
descriptionValue,
|
||||
allowPkce,
|
||||
// isPublic,
|
||||
changeValue,
|
||||
|
||||
isEdit,
|
||||
errorFields,
|
||||
requiredErrorFields,
|
||||
onBlur,
|
||||
}: BasicBlockProps) => {
|
||||
const onChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
const target = e.target;
|
||||
|
||||
changeValue(target.name as keyof IClientReqDTO, target.value);
|
||||
};
|
||||
|
||||
const onSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file =
|
||||
e.target.files && e.target.files?.length > 0 && e.target.files[0];
|
||||
|
||||
if (file) {
|
||||
const imgEl = document.getElementsByClassName(
|
||||
"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 widthRatioBlob = await compressImage(
|
||||
imgEl,
|
||||
MAX_WIDTH / width,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
const heightRatioBlob = await compressImage(
|
||||
imgEl,
|
||||
MAX_HEIGHT / height,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
if (widthRatioBlob && heightRatioBlob) {
|
||||
// pick the smaller blob between both
|
||||
const compressedBlob =
|
||||
widthRatioBlob.size > heightRatioBlob.size
|
||||
? heightRatioBlob
|
||||
: widthRatioBlob;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(compressedBlob);
|
||||
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
|
||||
changeValue("logo", result);
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pkceHelpButtonText = (
|
||||
<Trans t={t} i18nKey="AllowPKCEHelpButton" ns="OAuth" />
|
||||
);
|
||||
|
||||
// const publicClientHelpButtonText = "Help text";
|
||||
|
||||
const isNameRequiredError = requiredErrorFields.includes("name");
|
||||
const isWebsiteRequiredError = requiredErrorFields.includes("website_url");
|
||||
const isNameError = errorFields.includes("name");
|
||||
const isWebsiteError = errorFields.includes("website_url");
|
||||
const isLogoRequiredError = requiredErrorFields.includes("logo");
|
||||
|
||||
return (
|
||||
<StyledBlock>
|
||||
<BlockHeader header="Basic info" />
|
||||
<StyledInputBlock>
|
||||
<InputGroup
|
||||
label={t("AppName")}
|
||||
name="name"
|
||||
placeholder={t("Common:EnterName")}
|
||||
value={nameValue}
|
||||
error={isNameError ? `${t("ErrorName")} 3` : t("ThisRequiredField")}
|
||||
onChange={onChange}
|
||||
isRequired
|
||||
isError={isNameRequiredError || isNameError}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<InputGroup
|
||||
label={t("WebsiteUrl")}
|
||||
name="website_url"
|
||||
placeholder={t("EnterURL")}
|
||||
value={websiteUrlValue}
|
||||
error={
|
||||
isWebsiteError
|
||||
? `${t("ErrorWrongURL")}: ${window.location.origin}`
|
||||
: t("ThisRequiredField")
|
||||
}
|
||||
onChange={onChange}
|
||||
disabled={isEdit}
|
||||
isRequired
|
||||
isError={isWebsiteRequiredError || isWebsiteError}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<FieldContainer
|
||||
isVertical
|
||||
labelVisible={false}
|
||||
errorMessage={t("ThisRequiredField")}
|
||||
hasError={isLogoRequiredError}
|
||||
className="icon-field"
|
||||
>
|
||||
<SelectGroup
|
||||
label={t("AppIcon")}
|
||||
value={logoValue}
|
||||
selectLabel={t("SelectNewImage")}
|
||||
description={t("IconDescription")}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</FieldContainer>
|
||||
|
||||
<TextAreaGroup
|
||||
label={t("Common:Description")}
|
||||
name="description"
|
||||
placeholder={t("EnterDescription")}
|
||||
value={descriptionValue}
|
||||
onChange={onChange}
|
||||
increaseHeight={isLogoRequiredError}
|
||||
/>
|
||||
<InputGroup
|
||||
label={t("AuthenticationMethod")}
|
||||
name="auth_method"
|
||||
placeholder={t("EnterURL")}
|
||||
value={websiteUrlValue}
|
||||
error=""
|
||||
onChange={() => {}}
|
||||
>
|
||||
<div className="pkce">
|
||||
<Checkbox
|
||||
label={t("AllowPKCE")}
|
||||
isChecked={allowPkce}
|
||||
onChange={() => {
|
||||
changeValue("allow_pkce", !allowPkce);
|
||||
}}
|
||||
/>
|
||||
<HelpButton tooltipContent={pkceHelpButtonText} />
|
||||
</div>
|
||||
</InputGroup>
|
||||
{/* <InputGroup
|
||||
label="Client type"
|
||||
name="public_client"
|
||||
placeholder={t("EnterURL")}
|
||||
value=""
|
||||
error=""
|
||||
onChange={() => {}}
|
||||
>
|
||||
<div className="public_client">
|
||||
<ToggleButton
|
||||
isChecked={isPublic}
|
||||
onChange={(e) => {
|
||||
changeValue("is_public", e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<Text>Public client</Text>
|
||||
<HelpButton tooltipContent={publicClientHelpButtonText} />
|
||||
</div>
|
||||
</InputGroup> */}
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicBlock;
|
@ -0,0 +1,36 @@
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { HelpButton } from "@docspace/shared/components/help-button";
|
||||
|
||||
import { StyledHeaderRow } from "../ClientForm.styled";
|
||||
|
||||
interface BlockHeaderProps {
|
||||
header: string;
|
||||
helpButtonText?: string | React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BlockHeader = ({
|
||||
header,
|
||||
helpButtonText,
|
||||
className,
|
||||
}: BlockHeaderProps) => {
|
||||
return (
|
||||
<StyledHeaderRow className={className}>
|
||||
<Text
|
||||
fontSize="16px"
|
||||
fontWeight={700}
|
||||
lineHeight="22px"
|
||||
title={header}
|
||||
tag=""
|
||||
as="p"
|
||||
color=""
|
||||
textAlign=""
|
||||
>
|
||||
{header}
|
||||
</Text>
|
||||
{helpButtonText && <HelpButton tooltipContent={helpButtonText} />}
|
||||
</StyledHeaderRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockHeader;
|
@ -0,0 +1,57 @@
|
||||
import { DeviceType } from "@docspace/shared/enums";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
|
||||
import { StyledButtonContainer } from "../ClientForm.styled";
|
||||
|
||||
interface ButtonsBlockProps {
|
||||
saveLabel: string;
|
||||
cancelLabel: string;
|
||||
|
||||
isRequestRunning: boolean;
|
||||
|
||||
saveButtonDisabled: boolean;
|
||||
cancelButtonDisabled: boolean;
|
||||
|
||||
onSaveClick: () => void;
|
||||
onCancelClick: () => void;
|
||||
|
||||
currentDeviceType: string;
|
||||
}
|
||||
|
||||
const ButtonsBlock = ({
|
||||
saveLabel,
|
||||
cancelLabel,
|
||||
isRequestRunning,
|
||||
saveButtonDisabled,
|
||||
cancelButtonDisabled,
|
||||
onSaveClick,
|
||||
onCancelClick,
|
||||
currentDeviceType,
|
||||
}: ButtonsBlockProps) => {
|
||||
const isDesktop = currentDeviceType === DeviceType.desktop;
|
||||
|
||||
const buttonSize = isDesktop ? ButtonSize.small : ButtonSize.normal;
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
label={saveLabel}
|
||||
isLoading={isRequestRunning}
|
||||
isDisabled={saveButtonDisabled}
|
||||
primary
|
||||
size={buttonSize}
|
||||
scale={!isDesktop}
|
||||
onClick={onSaveClick}
|
||||
/>
|
||||
|
||||
<Button
|
||||
label={cancelLabel}
|
||||
isDisabled={cancelButtonDisabled}
|
||||
size={buttonSize}
|
||||
scale={!isDesktop}
|
||||
onClick={onCancelClick}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonsBlock;
|
@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
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";
|
||||
|
||||
import BlockHeader from "./BlockHeader";
|
||||
import InputGroup from "./InputGroup";
|
||||
|
||||
interface ClientBlockProps {
|
||||
t: TTranslation;
|
||||
|
||||
idValue: string;
|
||||
secretValue: string;
|
||||
|
||||
onResetClick: () => void;
|
||||
}
|
||||
|
||||
const ClientBlock = ({
|
||||
t,
|
||||
idValue,
|
||||
secretValue,
|
||||
onResetClick,
|
||||
}: ClientBlockProps) => {
|
||||
const [value, setValue] = React.useState<{ [key: string]: string }>({
|
||||
id: idValue,
|
||||
secret: secretValue,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue({ id: idValue, secret: secretValue });
|
||||
}, [idValue, secretValue]);
|
||||
|
||||
const onChange = () => {};
|
||||
|
||||
const onCopyClick = (name: string) => {
|
||||
if (name === "id") {
|
||||
copy(value[name]);
|
||||
toastr.success(t("ClientCopy"));
|
||||
} else {
|
||||
copy(value[name]);
|
||||
toastr.success(t("SecretCopy"));
|
||||
}
|
||||
};
|
||||
|
||||
const helpButtonText = <Trans t={t} i18nKey="ClientHelpButton" ns="OAuth" />;
|
||||
|
||||
return (
|
||||
<StyledBlock>
|
||||
<BlockHeader header={t("Client")} helpButtonText={helpButtonText} />
|
||||
<StyledInputBlock>
|
||||
<InputGroup
|
||||
label={t("ID")}
|
||||
name=""
|
||||
placeholder=""
|
||||
value={value.id}
|
||||
error=""
|
||||
onChange={onChange}
|
||||
withCopy
|
||||
onCopyClick={() => onCopyClick("id")}
|
||||
/>
|
||||
<InputGroup
|
||||
label={t("Secret")}
|
||||
name=""
|
||||
placeholder=""
|
||||
value={value.secret}
|
||||
error=""
|
||||
onChange={onChange}
|
||||
withCopy
|
||||
isPassword
|
||||
buttonLabel={t("Reset")}
|
||||
onButtonClick={onResetClick}
|
||||
onCopyClick={() => onCopyClick("secret")}
|
||||
/>
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientBlock;
|
@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
|
||||
import { InputBlock } from "@docspace/shared/components/input-block";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
import { FieldContainer } from "@docspace/shared/components/field-container";
|
||||
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
|
||||
import { InputSize, InputType } from "@docspace/shared/components/text-input";
|
||||
|
||||
import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url";
|
||||
|
||||
import { StyledInputGroup } from "../ClientForm.styled";
|
||||
|
||||
interface InputGroupProps {
|
||||
label: string;
|
||||
|
||||
name: string;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
|
||||
error: string;
|
||||
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
|
||||
helpButtonText?: string;
|
||||
|
||||
buttonLabel?: string;
|
||||
onButtonClick?: () => void;
|
||||
|
||||
withCopy?: boolean;
|
||||
onCopyClick?: (e: React.MouseEvent) => void;
|
||||
isPassword?: boolean;
|
||||
|
||||
disabled?: boolean;
|
||||
isRequired?: boolean;
|
||||
isError?: boolean;
|
||||
children?: React.ReactNode;
|
||||
|
||||
onBlur?: (name: string) => void;
|
||||
}
|
||||
|
||||
const InputGroup = ({
|
||||
label,
|
||||
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
|
||||
error,
|
||||
|
||||
onChange,
|
||||
onBlur,
|
||||
|
||||
helpButtonText,
|
||||
|
||||
buttonLabel,
|
||||
onButtonClick,
|
||||
|
||||
withCopy,
|
||||
onCopyClick,
|
||||
isPassword,
|
||||
disabled,
|
||||
isRequired,
|
||||
isError,
|
||||
children,
|
||||
}: InputGroupProps) => {
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
|
||||
const onButtonClickAction = async () => {
|
||||
setIsRequestRunning(true);
|
||||
|
||||
onButtonClick?.();
|
||||
|
||||
setTimeout(() => {
|
||||
setIsRequestRunning(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledInputGroup>
|
||||
<FieldContainer
|
||||
className={buttonLabel ? "input-block-with-button" : ""}
|
||||
isVertical
|
||||
isRequired={isRequired}
|
||||
labelVisible
|
||||
labelText={label}
|
||||
tooltipContent={helpButtonText}
|
||||
errorMessage={error}
|
||||
removeMargin
|
||||
hasError={isError}
|
||||
>
|
||||
{children || (
|
||||
<>
|
||||
{isRequestRunning ? (
|
||||
<RectangleSkeleton
|
||||
className="loader"
|
||||
width="100%"
|
||||
height="32px"
|
||||
/>
|
||||
) : (
|
||||
<InputBlock
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
scale
|
||||
tabIndex={0}
|
||||
maxLength={255}
|
||||
isReadOnly={withCopy}
|
||||
isDisabled={disabled}
|
||||
size={InputSize.base}
|
||||
iconName={withCopy ? CopyReactSvgUrl : ""}
|
||||
onIconClick={withCopy ? onCopyClick : undefined}
|
||||
type={isPassword ? InputType.password : InputType.text}
|
||||
onBlur={() => onBlur?.(name)}
|
||||
hasError={isError}
|
||||
/>
|
||||
)}
|
||||
{buttonLabel && (
|
||||
<Button
|
||||
label={buttonLabel}
|
||||
size={ButtonSize.small}
|
||||
onClick={onButtonClickAction}
|
||||
isDisabled={isRequestRunning}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FieldContainer>
|
||||
</StyledInputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputGroup;
|
@ -0,0 +1,192 @@
|
||||
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/types";
|
||||
|
||||
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";
|
||||
|
||||
interface MultiInputGroupProps {
|
||||
t: TTranslation;
|
||||
label: string;
|
||||
|
||||
name: string;
|
||||
placeholder: string;
|
||||
currentValue: string[];
|
||||
hasError?: boolean;
|
||||
onAdd: (name: keyof IClientReqDTO, value: string, remove?: boolean) => void;
|
||||
|
||||
helpButtonText?: string;
|
||||
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const MultiInputGroup = ({
|
||||
t,
|
||||
label,
|
||||
name,
|
||||
placeholder,
|
||||
currentValue,
|
||||
onAdd,
|
||||
hasError,
|
||||
helpButtonText,
|
||||
isDisabled,
|
||||
}: 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 addRef = React.useRef<null | HTMLDivElement>(null);
|
||||
|
||||
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);
|
||||
} else {
|
||||
setIsError(true);
|
||||
}
|
||||
} else {
|
||||
setIsError(false);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
label={label}
|
||||
helpButtonText={helpButtonText}
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
error={
|
||||
isError
|
||||
? `${t("ErrorWrongURL")}: ${window.location.origin}`
|
||||
: t("ThisRequiredField")
|
||||
}
|
||||
isRequired
|
||||
isError={isError || hasError}
|
||||
>
|
||||
<StyledInputRow>
|
||||
<InputBlock
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
scale
|
||||
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={onAddAction}
|
||||
isDisabled={isDisabled || isError}
|
||||
/>
|
||||
</StyledInputRow>
|
||||
</InputGroup>
|
||||
|
||||
<StyledChipsContainer>
|
||||
{currentValue.map((v) => (
|
||||
<SelectedItem
|
||||
key={`${v}`}
|
||||
propKey={v}
|
||||
isInline
|
||||
label={v}
|
||||
isDisabled={isDisabled}
|
||||
hideCross={isDisabled}
|
||||
onClose={() => {
|
||||
if (!isDisabled) onAdd(name as keyof IClientReqDTO, v, true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</StyledChipsContainer>
|
||||
</StyledInputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiInputGroup;
|
@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
import { IClientReqDTO } from "@docspace/shared/utils/oauth/types";
|
||||
|
||||
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
|
||||
|
||||
import BlockHeader from "./BlockHeader";
|
||||
import MultiInputGroup from "./MultiInputGroup";
|
||||
|
||||
interface OAuthBlockProps {
|
||||
t: TTranslation;
|
||||
|
||||
redirectUrisValue: string[];
|
||||
allowedOriginsValue: string[];
|
||||
|
||||
changeValue: (
|
||||
name: keyof IClientReqDTO,
|
||||
value: string,
|
||||
remove?: boolean,
|
||||
) => void;
|
||||
requiredErrorFields: string[];
|
||||
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
const OAuthBlock = ({
|
||||
t,
|
||||
redirectUrisValue,
|
||||
allowedOriginsValue,
|
||||
|
||||
changeValue,
|
||||
requiredErrorFields,
|
||||
isEdit,
|
||||
}: OAuthBlockProps) => {
|
||||
return (
|
||||
<StyledBlock>
|
||||
<BlockHeader header={t("OAuthHeaderBlock")} />
|
||||
<StyledInputBlock>
|
||||
<MultiInputGroup
|
||||
t={t}
|
||||
label={t("RedirectsURLS")}
|
||||
placeholder={t("EnterURL")}
|
||||
name="redirect_uris"
|
||||
onAdd={changeValue}
|
||||
currentValue={redirectUrisValue}
|
||||
helpButtonText={t("RedirectsURLSHelpButton")}
|
||||
isDisabled={isEdit}
|
||||
hasError={requiredErrorFields.includes("redirect_uris")}
|
||||
/>
|
||||
<MultiInputGroup
|
||||
t={t}
|
||||
label={t("AllowedOrigins")}
|
||||
placeholder={t("EnterURL")}
|
||||
name="allowed_origins"
|
||||
onAdd={changeValue}
|
||||
currentValue={allowedOriginsValue}
|
||||
helpButtonText={t("AllowedOriginsHelpButton")}
|
||||
hasError={requiredErrorFields.includes("allowed_origins")}
|
||||
/>
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthBlock;
|
@ -0,0 +1,224 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
IClientReqDTO,
|
||||
TFilteredScopes,
|
||||
TScope,
|
||||
} from "@docspace/shared/utils/oauth/types";
|
||||
import {
|
||||
filterScopeByGroup,
|
||||
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";
|
||||
|
||||
import BlockHeader from "./BlockHeader";
|
||||
|
||||
import {
|
||||
StyledScopesCheckbox,
|
||||
StyledScopesContainer,
|
||||
StyledScopesName,
|
||||
} from "../ClientForm.styled";
|
||||
|
||||
interface TScopesBlockProps {
|
||||
scopes: TScope[];
|
||||
selectedScopes: string[];
|
||||
onAddScope: (name: keyof IClientReqDTO, scope: string) => void;
|
||||
t: TTranslation;
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
const ScopesBlock = ({
|
||||
scopes,
|
||||
selectedScopes,
|
||||
onAddScope,
|
||||
t,
|
||||
isEdit,
|
||||
}: TScopesBlockProps) => {
|
||||
const [checkedScopes, setCheckedScopes] = React.useState<string[]>([]);
|
||||
const [filteredScopes, setFilteredScopes] = React.useState<TFilteredScopes>(
|
||||
filterScopeByGroup(selectedScopes, scopes),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const filtered = filterScopeByGroup(selectedScopes, scopes);
|
||||
|
||||
setCheckedScopes([...selectedScopes]);
|
||||
setFilteredScopes({ ...filtered });
|
||||
}, [scopes, selectedScopes]);
|
||||
|
||||
const onAddCheckedScope = (
|
||||
group: ScopeGroup,
|
||||
type: ScopeType,
|
||||
name: string = "",
|
||||
) => {
|
||||
const isChecked = checkedScopes.includes(name);
|
||||
|
||||
if (!isChecked) {
|
||||
setFilteredScopes((val) => {
|
||||
val[group].isChecked = true;
|
||||
val[group].checkedType = type;
|
||||
|
||||
return { ...val };
|
||||
});
|
||||
setCheckedScopes((val) => [...val, name]);
|
||||
} else {
|
||||
if (type === ScopeType.read) {
|
||||
setFilteredScopes((val) => {
|
||||
val[group].isChecked = false;
|
||||
val[group].checkedType = undefined;
|
||||
|
||||
return { ...val };
|
||||
});
|
||||
} else {
|
||||
setFilteredScopes((val) => {
|
||||
const isReadChecked = checkedScopes.includes(
|
||||
val[group].read?.name || "",
|
||||
);
|
||||
|
||||
val[group].isChecked = isReadChecked;
|
||||
val[group].checkedType = isReadChecked ? ScopeType.read : undefined;
|
||||
|
||||
return { ...val };
|
||||
});
|
||||
}
|
||||
|
||||
setCheckedScopes((val) => val.filter((v) => v !== name));
|
||||
}
|
||||
|
||||
onAddScope("scopes", name);
|
||||
};
|
||||
|
||||
const getRenderedScopeList = () => {
|
||||
const list: React.ReactNode[] = [];
|
||||
|
||||
Object.entries(filteredScopes).forEach(([key, value]) => {
|
||||
const name = getScopeTKeyName(key as ScopeGroup);
|
||||
|
||||
const isReadDisabled = value.checkedType === ScopeType.write;
|
||||
const isReadChecked = value.isChecked;
|
||||
|
||||
const row = (
|
||||
<React.Fragment key={name}>
|
||||
<StyledScopesName>
|
||||
<Text
|
||||
className="scope-name"
|
||||
fontSize="14px"
|
||||
fontWeight={600}
|
||||
lineHeight="16px"
|
||||
>
|
||||
{t(`Common:${name}`)}
|
||||
</Text>
|
||||
|
||||
{value.read?.name && (
|
||||
<Text
|
||||
className="scope-desc"
|
||||
fontSize="12px"
|
||||
fontWeight={400}
|
||||
lineHeight="16px"
|
||||
>
|
||||
<Text
|
||||
className="scope-desc"
|
||||
as="span"
|
||||
fontSize="12px"
|
||||
fontWeight={600}
|
||||
lineHeight="16px"
|
||||
>
|
||||
{value.read?.name}
|
||||
</Text>{" "}
|
||||
— {t(`Common:${value.read?.tKey}`)}
|
||||
</Text>
|
||||
)}
|
||||
{value.write?.name && (
|
||||
<Text
|
||||
className="scope-desc"
|
||||
fontSize="12px"
|
||||
fontWeight={400}
|
||||
lineHeight="16px"
|
||||
>
|
||||
<Text
|
||||
className="scope-desc"
|
||||
as="span"
|
||||
fontSize="12px"
|
||||
fontWeight={600}
|
||||
lineHeight="16px"
|
||||
>
|
||||
{value.write?.name}
|
||||
</Text>{" "}
|
||||
— {t(`Common:${value.write?.tKey}`)}
|
||||
</Text>
|
||||
)}
|
||||
</StyledScopesName>
|
||||
<StyledScopesCheckbox>
|
||||
<Checkbox
|
||||
className="checkbox-read"
|
||||
isChecked={isReadChecked}
|
||||
isDisabled={isReadDisabled || isEdit}
|
||||
onChange={() =>
|
||||
onAddCheckedScope(
|
||||
key as ScopeGroup,
|
||||
ScopeType.read,
|
||||
value.read?.name,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledScopesCheckbox>
|
||||
<StyledScopesCheckbox>
|
||||
{value.write?.name && (
|
||||
<Checkbox
|
||||
isChecked={isReadDisabled}
|
||||
isDisabled={isEdit || !value.write?.name}
|
||||
onChange={() =>
|
||||
onAddCheckedScope(
|
||||
key as ScopeGroup,
|
||||
ScopeType.write,
|
||||
value.write?.name,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledScopesCheckbox>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
list.push(row);
|
||||
});
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
const list = getRenderedScopeList();
|
||||
|
||||
return (
|
||||
<StyledScopesContainer>
|
||||
<BlockHeader
|
||||
className="header"
|
||||
header={t("ScopesHeader")}
|
||||
helpButtonText={t("ScopesHelp")}
|
||||
/>
|
||||
|
||||
<Text
|
||||
className="header"
|
||||
fontSize="14px"
|
||||
fontWeight={600}
|
||||
lineHeight="22px"
|
||||
>
|
||||
{t("Read")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className="header header-last"
|
||||
fontSize="14px"
|
||||
fontWeight={600}
|
||||
lineHeight="22px"
|
||||
>
|
||||
{t("Write")}
|
||||
</Text>
|
||||
{list.map((item) => item)}
|
||||
</StyledScopesContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScopesBlock;
|
@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { SelectorAddButton } from "@docspace/shared/components/selector-add-button";
|
||||
|
||||
import { StyledInputGroup } from "../ClientForm.styled";
|
||||
|
||||
interface SelectGroupProps {
|
||||
label: string;
|
||||
selectLabel: string;
|
||||
|
||||
value: string;
|
||||
|
||||
description: string;
|
||||
|
||||
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const SelectGroup = ({
|
||||
label,
|
||||
selectLabel,
|
||||
|
||||
value,
|
||||
|
||||
description,
|
||||
|
||||
onSelect,
|
||||
}: SelectGroupProps) => {
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onClick = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const onInputClick = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
|
||||
inputRef.current.files = null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledInputGroup>
|
||||
<div className="label">
|
||||
<Text
|
||||
fontSize="13px"
|
||||
fontWeight={600}
|
||||
lineHeight="20px"
|
||||
title=""
|
||||
tag=""
|
||||
as="p"
|
||||
color=""
|
||||
textAlign=""
|
||||
>
|
||||
{label} *
|
||||
</Text>
|
||||
</div>
|
||||
<div className="select">
|
||||
<img
|
||||
className="client-logo"
|
||||
style={{ display: value ? "block" : "none" }}
|
||||
alt="img"
|
||||
src={value}
|
||||
/>
|
||||
<SelectorAddButton onClick={onClick} />
|
||||
<Text
|
||||
fontSize="13px"
|
||||
fontWeight={600}
|
||||
lineHeight="20px"
|
||||
title=""
|
||||
tag=""
|
||||
as="p"
|
||||
color=""
|
||||
textAlign=""
|
||||
>
|
||||
{selectLabel}
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
fontSize="12px"
|
||||
fontWeight={600}
|
||||
lineHeight="16px"
|
||||
title=""
|
||||
tag=""
|
||||
as="p"
|
||||
color=""
|
||||
textAlign=""
|
||||
className="description"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="customFileInput"
|
||||
className="custom-file-input"
|
||||
multiple
|
||||
type="file"
|
||||
onChange={onSelect}
|
||||
onClick={onInputClick}
|
||||
style={{ display: "none" }}
|
||||
accept="image/png, image/jpeg, svg"
|
||||
/>
|
||||
</StyledInputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectGroup;
|
@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
import { IClientReqDTO } from "@docspace/shared/utils/oauth/types";
|
||||
|
||||
import { StyledBlock, StyledInputBlock } from "../ClientForm.styled";
|
||||
|
||||
import BlockHeader from "./BlockHeader";
|
||||
import InputGroup from "./InputGroup";
|
||||
|
||||
interface SupportBlockProps {
|
||||
t: TTranslation;
|
||||
|
||||
policyUrlValue: string;
|
||||
termsUrlValue: string;
|
||||
|
||||
changeValue: (name: keyof IClientReqDTO, value: string) => void;
|
||||
|
||||
isEdit: boolean;
|
||||
errorFields: string[];
|
||||
onBlur?: (name: string) => void;
|
||||
requiredErrorFields: string[];
|
||||
}
|
||||
|
||||
const SupportBlock = ({
|
||||
t,
|
||||
policyUrlValue,
|
||||
termsUrlValue,
|
||||
|
||||
changeValue,
|
||||
|
||||
isEdit,
|
||||
errorFields,
|
||||
onBlur,
|
||||
requiredErrorFields,
|
||||
}: SupportBlockProps) => {
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = e.target;
|
||||
|
||||
changeValue(target.name as keyof IClientReqDTO, target.value);
|
||||
};
|
||||
|
||||
const policyRequiredError = requiredErrorFields.includes("policy_url");
|
||||
const termsRequiredError = requiredErrorFields.includes("terms_url");
|
||||
const policyError = errorFields.includes("policy_url");
|
||||
const termsError = errorFields.includes("terms_url");
|
||||
|
||||
return (
|
||||
<StyledBlock>
|
||||
<BlockHeader header={t("SupportAndLegalInfo")} />
|
||||
<StyledInputBlock>
|
||||
<InputGroup
|
||||
label={t("PrivacyPolicyURL")}
|
||||
name="policy_url"
|
||||
placeholder={t("EnterURL")}
|
||||
value={policyUrlValue}
|
||||
error={
|
||||
policyError
|
||||
? `${t("ErrorWrongURL")}: ${window.location.origin}`
|
||||
: t("ThisRequiredField")
|
||||
}
|
||||
onChange={onChange}
|
||||
helpButtonText={t("PrivacyPolicyURLHelpButton")}
|
||||
disabled={isEdit}
|
||||
isRequired
|
||||
isError={policyError || policyRequiredError}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<InputGroup
|
||||
label={t("TermsOfServiceURL")}
|
||||
name="terms_url"
|
||||
placeholder={t("EnterURL")}
|
||||
value={termsUrlValue}
|
||||
error={
|
||||
termsError
|
||||
? `${t("ErrorWrongURL")}: ${window.location.origin}`
|
||||
: t("ThisRequiredField")
|
||||
}
|
||||
onChange={onChange}
|
||||
helpButtonText={t("TermsOfServiceURLHelpButton")}
|
||||
disabled={isEdit}
|
||||
isRequired
|
||||
isError={termsError || termsRequiredError}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</StyledInputBlock>
|
||||
</StyledBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportBlock;
|
@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Textarea } from "@docspace/shared/components/textarea";
|
||||
|
||||
import { StyledInputGroup } from "../ClientForm.styled";
|
||||
|
||||
interface TextAreaProps {
|
||||
label: string;
|
||||
name: string;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
increaseHeight: boolean;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
}
|
||||
|
||||
const TextAreaGroup = ({
|
||||
label,
|
||||
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
increaseHeight,
|
||||
|
||||
onChange,
|
||||
}: TextAreaProps) => {
|
||||
return (
|
||||
<StyledInputGroup>
|
||||
<div className="label">
|
||||
<Text
|
||||
fontSize="13px"
|
||||
fontWeight={600}
|
||||
lineHeight="20px"
|
||||
title=""
|
||||
tag=""
|
||||
as="p"
|
||||
color=""
|
||||
textAlign=""
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
<Textarea
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
tabIndex={0}
|
||||
heightTextArea={increaseHeight ? 81 : 60}
|
||||
maxLength={255}
|
||||
/>
|
||||
</StyledInputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextAreaGroup;
|
@ -0,0 +1,484 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
IClientProps,
|
||||
IClientReqDTO,
|
||||
} from "@docspace/shared/utils/oauth/types";
|
||||
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";
|
||||
|
||||
import BasicBlock from "./components/BasicBlock";
|
||||
import ClientBlock from "./components/ClientBlock";
|
||||
import SupportBlock from "./components/SupportBlock";
|
||||
import OAuthBlock from "./components/OAuthBlock";
|
||||
import ScopesBlock from "./components/ScopesBlock";
|
||||
import ButtonsBlock from "./components/ButtonsBlock";
|
||||
|
||||
import { StyledContainer } from "./ClientForm.styled";
|
||||
import { ClientFormProps, ClientStore } from "./ClientForm.types";
|
||||
import { isValidUrl } from "./ClientForm.utils";
|
||||
|
||||
import ClientFormLoader from "./Loader";
|
||||
|
||||
const ClientForm = ({
|
||||
id,
|
||||
|
||||
client,
|
||||
|
||||
scopeList,
|
||||
|
||||
fetchScopes,
|
||||
|
||||
saveClient,
|
||||
updateClient,
|
||||
|
||||
resetDialogVisible,
|
||||
setResetDialogVisible,
|
||||
|
||||
clientSecretProps,
|
||||
setClientSecretProps,
|
||||
|
||||
currentDeviceType,
|
||||
}: ClientFormProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [isRequestRunning, setIsRequestRunning] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
const [initialClient, setInitialClient] = React.useState<IClientProps>(
|
||||
{} as IClientProps,
|
||||
);
|
||||
const [form, setForm] = React.useState<IClientReqDTO>({
|
||||
name: "",
|
||||
logo: "",
|
||||
website_url: "",
|
||||
description: "",
|
||||
|
||||
redirect_uris: [],
|
||||
allowed_origins: [],
|
||||
logout_redirect_uri: "",
|
||||
|
||||
terms_url: "",
|
||||
policy_url: "",
|
||||
|
||||
is_public: true,
|
||||
allow_pkce: false,
|
||||
|
||||
scopes: [],
|
||||
});
|
||||
|
||||
const [errorFields, setErrorFields] = React.useState<string[]>([]);
|
||||
const [requiredErrorFields, setRequiredErrorFields] = React.useState<
|
||||
string[]
|
||||
>([]);
|
||||
|
||||
const { t } = useTranslation(["OAuth", "Common"]);
|
||||
|
||||
const [clientId, setClientId] = React.useState<string>("");
|
||||
const [clientSecret, setClientSecret] = React.useState<string>("");
|
||||
|
||||
const isEdit = !!id;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (clientSecretProps) {
|
||||
setClientSecret(clientSecretProps);
|
||||
setClientSecretProps?.("");
|
||||
}
|
||||
}, [clientSecretProps, setClientSecretProps]);
|
||||
|
||||
const onCancelClick = () => {
|
||||
navigate("/portal-settings/developer-tools/oauth");
|
||||
};
|
||||
|
||||
const onSaveClick = async () => {
|
||||
try {
|
||||
if (!id) {
|
||||
let isValid = true;
|
||||
|
||||
Object.entries(form).forEach(([key, value]) => {
|
||||
if (key === "description" || key === "logout_redirect_uri") return;
|
||||
|
||||
if (
|
||||
(value === "" && typeof value === "string") ||
|
||||
(value.length === 0 && value instanceof Array)
|
||||
) {
|
||||
if (!requiredErrorFields.includes(key))
|
||||
setRequiredErrorFields((s) => [...s, key]);
|
||||
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
isValid = isValid && !errorFields.includes(key);
|
||||
|
||||
if (key === "website_url" && !isValidUrl(value)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
setIsRequestRunning(true);
|
||||
|
||||
await saveClient?.(form);
|
||||
} else {
|
||||
await updateClient?.(clientId, form);
|
||||
}
|
||||
|
||||
onCancelClick();
|
||||
} catch (e) {
|
||||
toastr.error(e as unknown as TData);
|
||||
}
|
||||
};
|
||||
|
||||
const onResetClick = React.useCallback(async () => {
|
||||
setResetDialogVisible?.(true);
|
||||
}, [setResetDialogVisible]);
|
||||
|
||||
const onChangeForm = (
|
||||
name: keyof IClientReqDTO,
|
||||
value: string | boolean,
|
||||
remove?: boolean,
|
||||
) => {
|
||||
setForm((val) => {
|
||||
if (!(name in val)) return val;
|
||||
|
||||
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 {
|
||||
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 (clientId) return;
|
||||
|
||||
const actions = [];
|
||||
|
||||
if (id && !client) {
|
||||
actions.push(getClient(id));
|
||||
}
|
||||
|
||||
if (scopeList?.length === 0) actions.push(fetchScopes?.());
|
||||
|
||||
try {
|
||||
if (actions.length > 0) setIsLoading(true);
|
||||
|
||||
const [fetchedClient] = await Promise.all(actions);
|
||||
|
||||
const item = fetchedClient ?? client;
|
||||
|
||||
if (id && item) {
|
||||
setForm({
|
||||
name: item.name,
|
||||
logo: item.logo,
|
||||
website_url: item.websiteUrl,
|
||||
description: item.description ?? "",
|
||||
|
||||
redirect_uris: item.redirectUris ? [...item.redirectUris] : [],
|
||||
allowed_origins: item.allowedOrigins ? [...item.allowedOrigins] : [],
|
||||
logout_redirect_uri: item.logoutRedirectUri ?? "",
|
||||
|
||||
terms_url: item.termsUrl ?? "",
|
||||
policy_url: item.policyUrl ?? "",
|
||||
|
||||
allow_pkce: item.authenticationMethods
|
||||
? item.authenticationMethods.includes(AuthenticationMethod.none)
|
||||
: false,
|
||||
is_public: item.isPublic ?? false,
|
||||
|
||||
scopes: item.scopes ? [...item.scopes] : [],
|
||||
});
|
||||
|
||||
setClientId(item.clientId ?? " ");
|
||||
setClientSecret(item.clientSecret ?? " ");
|
||||
|
||||
setInitialClient(item ?? ({} as IClientProps));
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
|
||||
toastr.error(e as unknown as TData);
|
||||
}
|
||||
}, [clientId, id, client, scopeList?.length, fetchScopes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
getClientData();
|
||||
}, [getClientData]);
|
||||
|
||||
const onBlur = (key: string) => {
|
||||
if (
|
||||
key === "name" &&
|
||||
form[key] &&
|
||||
!errorFields.includes(key) &&
|
||||
(form[key].length < 3 || form[key].length > 256)
|
||||
) {
|
||||
setErrorFields((value) => {
|
||||
return [...value, key];
|
||||
});
|
||||
} else if (
|
||||
(key === "website_url" || key === "terms_url" || key === "policy_url") &&
|
||||
form[key] &&
|
||||
!errorFields.includes(key) &&
|
||||
!isValidUrl(form[key])
|
||||
) {
|
||||
setErrorFields((value) => {
|
||||
return [...value, key];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const compareAndValidate = () => {
|
||||
let isValid = true;
|
||||
|
||||
if (isEdit) {
|
||||
Object.entries(form).forEach(([key, value]) => {
|
||||
switch (key) {
|
||||
case "name":
|
||||
isValid = isValid && !!value;
|
||||
|
||||
if (
|
||||
value &&
|
||||
!errorFields.includes(key) &&
|
||||
(value.length < 3 || value.length > 256)
|
||||
) {
|
||||
isValid = false;
|
||||
|
||||
setErrorFields((val) => {
|
||||
return [...val, key];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
errorFields.includes(key) &&
|
||||
(!value || (value.length > 2 && value.length < 257))
|
||||
) {
|
||||
setErrorFields((val) => {
|
||||
return val.filter((n) => n !== key);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isValid = isValid && !errorFields.includes(key);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
(isValid &&
|
||||
form.name &&
|
||||
form.logo &&
|
||||
form.allowed_origins.length > 0 &&
|
||||
(form.name !== initialClient.name ||
|
||||
form.logo !== initialClient.logo ||
|
||||
form.description !== initialClient.description ||
|
||||
form.allowed_origins.length !==
|
||||
initialClient.allowedOrigins.length ||
|
||||
form.allow_pkce !==
|
||||
initialClient.authenticationMethods.includes(
|
||||
AuthenticationMethod.none,
|
||||
))) ||
|
||||
form.is_public !== initialClient.isPublic
|
||||
);
|
||||
}
|
||||
|
||||
Object.entries(form).forEach(([key, value]) => {
|
||||
switch (key) {
|
||||
case "name":
|
||||
case "logo":
|
||||
case "terms_url":
|
||||
case "policy_url":
|
||||
case "website_url":
|
||||
if (
|
||||
errorFields.includes(key) &&
|
||||
(!value || (value.length > 2 && value.length < 256))
|
||||
) {
|
||||
if (
|
||||
(key === "website_url" && isValidUrl(value)) ||
|
||||
key !== "website_url"
|
||||
)
|
||||
setErrorFields((val) => {
|
||||
return val.filter((n) => n !== 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) && value.length > 0)
|
||||
setRequiredErrorFields((val) => val.filter((v) => v !== key));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const isValid = compareAndValidate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer>
|
||||
{isLoading ? (
|
||||
<ClientFormLoader
|
||||
isEdit={isEdit}
|
||||
currentDeviceType={currentDeviceType}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<BasicBlock
|
||||
t={t}
|
||||
nameValue={form.name}
|
||||
websiteUrlValue={form.website_url}
|
||||
descriptionValue={form.description}
|
||||
logoValue={form.logo}
|
||||
allowPkce={form.allow_pkce}
|
||||
// isPublic={form.is_public}
|
||||
changeValue={onChangeForm}
|
||||
isEdit={isEdit}
|
||||
errorFields={errorFields}
|
||||
requiredErrorFields={requiredErrorFields}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{isEdit && (
|
||||
<ClientBlock
|
||||
t={t}
|
||||
idValue={clientId}
|
||||
secretValue={clientSecret}
|
||||
onResetClick={onResetClick}
|
||||
/>
|
||||
)}
|
||||
<OAuthBlock
|
||||
t={t}
|
||||
redirectUrisValue={form.redirect_uris}
|
||||
allowedOriginsValue={form.allowed_origins}
|
||||
changeValue={onChangeForm}
|
||||
isEdit={isEdit}
|
||||
requiredErrorFields={requiredErrorFields}
|
||||
/>
|
||||
<ScopesBlock
|
||||
t={t}
|
||||
scopes={scopeList || []}
|
||||
selectedScopes={form.scopes}
|
||||
onAddScope={onChangeForm}
|
||||
isEdit={isEdit}
|
||||
/>
|
||||
<SupportBlock
|
||||
t={t}
|
||||
policyUrlValue={form.policy_url}
|
||||
termsUrlValue={form.terms_url}
|
||||
changeValue={onChangeForm}
|
||||
isEdit={isEdit}
|
||||
errorFields={errorFields}
|
||||
requiredErrorFields={requiredErrorFields}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<ButtonsBlock
|
||||
saveLabel={t("Common:SaveButton")}
|
||||
cancelLabel={t("Common:CancelButton")}
|
||||
onSaveClick={onSaveClick}
|
||||
onCancelClick={onCancelClick}
|
||||
isRequestRunning={isRequestRunning}
|
||||
saveButtonDisabled={isEdit ? !isValid : false}
|
||||
cancelButtonDisabled={isRequestRunning}
|
||||
currentDeviceType={currentDeviceType || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
{resetDialogVisible && <ResetDialog />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({ oauthStore, settingsStore }: ClientStore, { id }: ClientFormProps) => {
|
||||
const {
|
||||
clientList,
|
||||
scopeList,
|
||||
|
||||
fetchScopes,
|
||||
|
||||
saveClient,
|
||||
updateClient,
|
||||
|
||||
setResetDialogVisible,
|
||||
resetDialogVisible,
|
||||
|
||||
setClientSecret,
|
||||
clientSecret,
|
||||
} = oauthStore;
|
||||
|
||||
const { currentDeviceType } = settingsStore;
|
||||
|
||||
const props: ClientFormProps = {
|
||||
scopeList,
|
||||
|
||||
fetchScopes,
|
||||
|
||||
saveClient,
|
||||
updateClient,
|
||||
|
||||
setResetDialogVisible,
|
||||
currentDeviceType,
|
||||
resetDialogVisible,
|
||||
setClientSecretProps: setClientSecret,
|
||||
clientSecretProps: clientSecret,
|
||||
};
|
||||
|
||||
if (id) {
|
||||
const client = clientList.find((c: IClientProps) => c.clientId === id);
|
||||
|
||||
props.client = client;
|
||||
}
|
||||
|
||||
return { ...props };
|
||||
},
|
||||
)(observer(ClientForm));
|
@ -0,0 +1,98 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
|
||||
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
import { TData } from "@docspace/shared/components/toast/Toast.type";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
interface DeleteClientDialogProps {
|
||||
isVisible?: boolean;
|
||||
onClose?: () => void;
|
||||
onDisable?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DeleteClientDialog = (props: DeleteClientDialogProps) => {
|
||||
const { t, ready } = useTranslation(["OAuth", "Common"]);
|
||||
const { isVisible, onClose, onDisable } = props;
|
||||
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
|
||||
const onDisableClick = async () => {
|
||||
try {
|
||||
setIsRequestRunning(true);
|
||||
await onDisable?.();
|
||||
|
||||
setIsRequestRunning(true);
|
||||
onClose?.();
|
||||
} catch (error: unknown) {
|
||||
const e = error as TData;
|
||||
toastr.error(e);
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
isLoading={!ready}
|
||||
visible={isVisible}
|
||||
onClose={onClose}
|
||||
displayType={ModalDialogType.modal}
|
||||
>
|
||||
<ModalDialog.Header>{t("DeleteHeader")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<Trans t={t} i18nKey="DeleteDescription" ns="OAuth" />
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<Button
|
||||
className="delete-button"
|
||||
key="DeletePortalBtn"
|
||||
label={t("Common:OkButton")}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
primary
|
||||
isLoading={isRequestRunning}
|
||||
onClick={onDisableClick}
|
||||
/>
|
||||
<Button
|
||||
className="cancel-button"
|
||||
key="CancelDeleteBtn"
|
||||
label={t("Common:CancelButton")}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
isDisabled={isRequestRunning}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
|
||||
const {
|
||||
bufferSelection,
|
||||
setDeleteDialogVisible,
|
||||
setActiveClient,
|
||||
setSelection,
|
||||
deleteClient,
|
||||
deleteDialogVisible,
|
||||
} = oauthStore;
|
||||
|
||||
const onClose = () => {
|
||||
setDeleteDialogVisible(false);
|
||||
};
|
||||
|
||||
const onDisable = async () => {
|
||||
if (!bufferSelection) return;
|
||||
setActiveClient(bufferSelection.clientId);
|
||||
await deleteClient([bufferSelection.clientId]);
|
||||
setActiveClient("");
|
||||
setSelection("");
|
||||
};
|
||||
|
||||
return { isVisible: deleteDialogVisible, onClose, onDisable };
|
||||
})(observer(DeleteClientDialog));
|
@ -0,0 +1,99 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
|
||||
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
import { TData } from "@docspace/shared/components/toast/Toast.type";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
interface DisableClientDialogProps {
|
||||
isVisible?: boolean;
|
||||
onClose?: () => void;
|
||||
onDisable?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DisableClientDialog = (props: DisableClientDialogProps) => {
|
||||
const { t, ready } = useTranslation(["OAuth", "Common"]);
|
||||
const { isVisible, onClose, onDisable } = props;
|
||||
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
|
||||
const onDisableClick = async () => {
|
||||
try {
|
||||
setIsRequestRunning(true);
|
||||
await onDisable?.();
|
||||
|
||||
setIsRequestRunning(true);
|
||||
onClose?.();
|
||||
} catch (error: unknown) {
|
||||
const e = error as TData;
|
||||
toastr.error(e);
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
isLoading={!ready}
|
||||
visible={isVisible}
|
||||
onClose={onClose}
|
||||
displayType={ModalDialogType.modal}
|
||||
>
|
||||
<ModalDialog.Header>{t("DisableApplication")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<Trans t={t} i18nKey="DisableApplicationDescription" ns="OAuth" />
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<Button
|
||||
className="delete-button"
|
||||
key="DeletePortalBtn"
|
||||
label={t("Common:OkButton")}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
primary
|
||||
isLoading={isRequestRunning}
|
||||
onClick={onDisableClick}
|
||||
/>
|
||||
<Button
|
||||
className="cancel-button"
|
||||
key="CancelDeleteBtn"
|
||||
label={t("Common:CancelButton")}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
isDisabled={isRequestRunning}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
|
||||
const {
|
||||
bufferSelection,
|
||||
setDisableDialogVisible,
|
||||
setActiveClient,
|
||||
setSelection,
|
||||
changeClientStatus,
|
||||
disableDialogVisible,
|
||||
} = oauthStore;
|
||||
|
||||
const onClose = () => {
|
||||
setDisableDialogVisible(false);
|
||||
};
|
||||
|
||||
const onDisable = async () => {
|
||||
if (!bufferSelection) return;
|
||||
|
||||
setActiveClient(bufferSelection.clientId);
|
||||
await changeClientStatus(bufferSelection.clientId, false);
|
||||
setActiveClient("");
|
||||
setSelection("");
|
||||
};
|
||||
|
||||
return { isVisible: disableDialogVisible, onClose, onDisable };
|
||||
})(observer(DisableClientDialog));
|
@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { EmptyScreenContainer } from "@docspace/shared/components/empty-screen-container";
|
||||
|
||||
import EmptyScreenOauthSvgUrl from "PUBLIC_DIR/images/empty_screen_oauth.svg?url";
|
||||
|
||||
import RegisterNewButton from "../RegisterNewButton";
|
||||
|
||||
const OAuthEmptyScreen = () => {
|
||||
const { t } = useTranslation(["OAuth"]);
|
||||
|
||||
return (
|
||||
<EmptyScreenContainer
|
||||
imageSrc={EmptyScreenOauthSvgUrl}
|
||||
imageAlt="Empty oauth list"
|
||||
headerText={t("NoOAuthAppHeader")}
|
||||
subheadingText={t("OAuthAppDescription")}
|
||||
buttons={<RegisterNewButton />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthEmptyScreen;
|
@ -0,0 +1,443 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { IClientProps, TScope } from "@docspace/shared/utils/oauth/types";
|
||||
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
|
||||
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
|
||||
import { getCookie } from "@docspace/shared/utils/cookie";
|
||||
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
|
||||
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import {
|
||||
ContextMenuButton,
|
||||
ContextMenuButtonDisplayType,
|
||||
} from "@docspace/shared/components/context-menu-button";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarRole,
|
||||
AvatarSize,
|
||||
} from "@docspace/shared/components/avatar";
|
||||
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
|
||||
import { Base } from "@docspace/shared/themes";
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
const StyledContainer = styled.div<{
|
||||
showDescription: boolean;
|
||||
withShowText: boolean;
|
||||
}>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
padding-top: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.client-block {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 12px;
|
||||
|
||||
.client-block__info {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
.client-block__info-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
max-width: 32px;
|
||||
max-height: 32px;
|
||||
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
max-height: ${(props) =>
|
||||
props.showDescription ? "100%" : props.withShowText ? "100px" : "100%"};
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
margin-bottom: ${(props) => (props.withShowText ? "4px" : 0)};
|
||||
}
|
||||
|
||||
.desc-link {
|
||||
color: ${(props) => props.theme.oauth.infoDialog.descLinkColor};
|
||||
}
|
||||
|
||||
.block-header {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
color: ${(props) => props.theme.oauth.infoDialog.blockHeaderColor};
|
||||
}
|
||||
|
||||
.creator-block {
|
||||
margin: 8px 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.privacy-block {
|
||||
display: flex;
|
||||
|
||||
.separator {
|
||||
display: inline-block;
|
||||
|
||||
margin-top: 2px;
|
||||
|
||||
height: 16px;
|
||||
width: 1px;
|
||||
|
||||
margin: 0 8px;
|
||||
|
||||
background: ${(props) => props.theme.oauth.infoDialog.separatorColor};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledContainer.defaultProps = { theme: Base };
|
||||
|
||||
interface InfoDialogProps {
|
||||
visible: boolean;
|
||||
scopeList?: TScope[];
|
||||
|
||||
setInfoDialogVisible?: (value: boolean) => void;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo?: boolean,
|
||||
isSettings?: boolean,
|
||||
) => ContextMenuModel[];
|
||||
|
||||
client?: IClientProps;
|
||||
isProfile?: boolean;
|
||||
}
|
||||
|
||||
const InfoDialog = ({
|
||||
visible,
|
||||
|
||||
client,
|
||||
scopeList,
|
||||
|
||||
setInfoDialogVisible,
|
||||
getContextMenuItems,
|
||||
|
||||
isProfile,
|
||||
}: InfoDialogProps) => {
|
||||
const { t } = useTranslation(["OAuth", "Common"]);
|
||||
|
||||
const [showDescription, setShowDescription] = React.useState(false);
|
||||
const [isRender, setIsRender] = React.useState(false);
|
||||
const [withShowText, setWithShowText] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsRender(true);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = document.getElementById("client-info-description-text");
|
||||
if (!el) return;
|
||||
|
||||
setWithShowText(el?.offsetHeight >= 100);
|
||||
}, [isRender]);
|
||||
|
||||
const getContextOptions = () => {
|
||||
const contextOptions =
|
||||
client && getContextMenuItems
|
||||
? getContextMenuItems(t, client, true, !isProfile)
|
||||
: [];
|
||||
|
||||
return contextOptions;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setInfoDialogVisible?.(false);
|
||||
};
|
||||
|
||||
const locale = getCookie("asc_language");
|
||||
|
||||
const modifiedDate = getCorrectDate(locale || "", client?.modifiedOn || "");
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
visible={visible}
|
||||
displayType={ModalDialogType.aside}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalDialog.Header>{t("Common:Info")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<StyledContainer
|
||||
showDescription={showDescription}
|
||||
withShowText={withShowText}
|
||||
>
|
||||
<div className="client-block">
|
||||
<div className="client-block__info">
|
||||
<img
|
||||
className="client-block__info-logo"
|
||||
alt="client-logo"
|
||||
src={client?.logo}
|
||||
/>
|
||||
|
||||
<Text
|
||||
fontSize="16px"
|
||||
lineHeight="22px"
|
||||
fontWeight="700"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{client?.name}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<ContextMenuButton
|
||||
displayType={ContextMenuButtonDisplayType.dropdown}
|
||||
getData={getContextOptions}
|
||||
/>
|
||||
</div>
|
||||
{!isProfile && (
|
||||
<>
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("Creator")}
|
||||
</Text>
|
||||
<div className="creator-block">
|
||||
<Avatar
|
||||
source={client?.creatorAvatar || ""}
|
||||
size={AvatarSize.min}
|
||||
role={AvatarRole.user}
|
||||
/>
|
||||
|
||||
<Text
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{client?.creatorDisplayName}
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isProfile && (
|
||||
<>
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("Common:Description")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
id="client-info-description-text"
|
||||
className="description"
|
||||
fontSize="13px"
|
||||
lineHeight="20px"
|
||||
fontWeight="400"
|
||||
noSelect
|
||||
>
|
||||
{client?.description}
|
||||
</Text>
|
||||
{withShowText && (
|
||||
<Link
|
||||
className="desc-link"
|
||||
fontSize="13px"
|
||||
lineHeight="15px"
|
||||
fontWeight="600"
|
||||
isHovered
|
||||
onClick={() => setShowDescription((val) => !val)}
|
||||
type={LinkType.action}
|
||||
>
|
||||
{showDescription ? "Hide" : "Show more"}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("Common:Website")}
|
||||
</Text>
|
||||
|
||||
<Link
|
||||
fontSize="13px"
|
||||
lineHeight="15px"
|
||||
fontWeight="600"
|
||||
isHovered
|
||||
href={client?.websiteUrl}
|
||||
type={LinkType.action}
|
||||
target={LinkTarget.blank}
|
||||
>
|
||||
{client?.websiteUrl}
|
||||
</Link>
|
||||
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("Access")}
|
||||
</Text>
|
||||
<ScopeList
|
||||
selectedScopes={client?.scopes || []}
|
||||
scopes={scopeList || []}
|
||||
t={t}
|
||||
/>
|
||||
{isProfile && (
|
||||
<>
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("AccessGranted")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fontSize="13px"
|
||||
lineHeight="20px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{modifiedDate}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="20px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("SupportAndLegalInfo")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className="privacy-block"
|
||||
fontSize="13px"
|
||||
lineHeight="15px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
<Link
|
||||
fontSize="13px"
|
||||
lineHeight="15px"
|
||||
fontWeight="600"
|
||||
isHovered
|
||||
href={client?.policyUrl}
|
||||
type={LinkType.action}
|
||||
target={LinkTarget.blank}
|
||||
>
|
||||
{t("PrivacyPolicy")}
|
||||
</Link>
|
||||
<span className="separator" />
|
||||
|
||||
<Link
|
||||
fontSize="13px"
|
||||
lineHeight="15px"
|
||||
fontWeight="600"
|
||||
isHovered
|
||||
href={client?.termsUrl}
|
||||
type={LinkType.action}
|
||||
target={LinkTarget.blank}
|
||||
>
|
||||
{t("Terms of Service")}
|
||||
</Link>
|
||||
</Text>
|
||||
{!isProfile && (
|
||||
<>
|
||||
<Text
|
||||
className="block-header"
|
||||
fontSize="14px"
|
||||
lineHeight="16px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{t("LastModified")}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fontSize="13px"
|
||||
lineHeight="20px"
|
||||
fontWeight="600"
|
||||
noSelect
|
||||
truncate
|
||||
>
|
||||
{modifiedDate}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
</ModalDialog.Body>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
|
||||
const {
|
||||
setInfoDialogVisible,
|
||||
bufferSelection,
|
||||
scopeList,
|
||||
getContextMenuItems,
|
||||
} = oauthStore;
|
||||
|
||||
return {
|
||||
setInfoDialogVisible,
|
||||
client: bufferSelection,
|
||||
scopeList,
|
||||
getContextMenuItems,
|
||||
};
|
||||
})(observer(InfoDialog));
|
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { RectangleSkeleton } from "@docspace/shared/skeletons/rectangle";
|
||||
import { TableSkeleton } from "@docspace/shared/skeletons/table";
|
||||
import { RowsSkeleton } from "@docspace/shared/skeletons/rows";
|
||||
|
||||
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
|
||||
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
|
||||
|
||||
import { OAuthContainer } from "../../StyledOAuth";
|
||||
import { StyledContainer } from ".";
|
||||
|
||||
const OAuthLoader = ({
|
||||
viewAs,
|
||||
currentDeviceType,
|
||||
}: {
|
||||
viewAs: ViewAsType;
|
||||
currentDeviceType: DeviceUnionType;
|
||||
}) => {
|
||||
const buttonHeight = currentDeviceType !== "desktop" ? "40px" : "32px";
|
||||
|
||||
return (
|
||||
<OAuthContainer>
|
||||
<StyledContainer>
|
||||
<RectangleSkeleton className="description" width="100%" height="16px" />
|
||||
<RectangleSkeleton
|
||||
className="add-button"
|
||||
width="220px"
|
||||
height={buttonHeight}
|
||||
/>
|
||||
{viewAs === "table" ? (
|
||||
<TableSkeleton style={{}} />
|
||||
) : (
|
||||
<RowsSkeleton style={{}} />
|
||||
)}
|
||||
</StyledContainer>
|
||||
</OAuthContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthLoader;
|
@ -0,0 +1,83 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Row } from "@docspace/shared/components/row";
|
||||
|
||||
import { RowContent } from "./RowContent";
|
||||
import { RowProps } from "./RowView.types";
|
||||
|
||||
export const OAuthRow = (props: RowProps) => {
|
||||
const {
|
||||
item,
|
||||
sectionWidth,
|
||||
changeClientStatus,
|
||||
isChecked,
|
||||
inProgress,
|
||||
getContextMenuItems,
|
||||
setSelection,
|
||||
} = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation(["OAuth", "Common", "Files"]);
|
||||
|
||||
const editClient = () => {
|
||||
navigate(`${item.clientId}`);
|
||||
};
|
||||
|
||||
const handleToggleEnabled = async () => {
|
||||
if (!changeClientStatus) return;
|
||||
await changeClientStatus(item.clientId, !item.enabled);
|
||||
};
|
||||
|
||||
const handleRowClick = (e: React.MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest(".checkbox") ||
|
||||
target.closest(".table-container_row-checkbox") ||
|
||||
e.detail === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
target.closest(".table-container_row-context-menu-wrapper") ||
|
||||
target.closest(".toggleButton") ||
|
||||
target.closest(".row_context-menu-wrapper")
|
||||
) {
|
||||
return setSelection && setSelection("");
|
||||
}
|
||||
|
||||
editClient();
|
||||
};
|
||||
|
||||
const contextOptions = getContextMenuItems && getContextMenuItems(t, item);
|
||||
|
||||
const element = (
|
||||
<img style={{ borderRadius: "3px" }} src={item.logo} alt="App logo" />
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
key={item.clientId}
|
||||
contextOptions={contextOptions}
|
||||
onRowClick={handleRowClick}
|
||||
element={element}
|
||||
mode="modern"
|
||||
checked={isChecked}
|
||||
inProgress={inProgress}
|
||||
onSelect={() => setSelection && setSelection(item.clientId)}
|
||||
className={`oauth2-row${isChecked ? " oauth2-row-selected" : ""}`}
|
||||
>
|
||||
<RowContent
|
||||
sectionWidth={sectionWidth}
|
||||
item={item}
|
||||
isChecked={isChecked}
|
||||
inProgress={inProgress}
|
||||
setSelection={setSelection}
|
||||
handleToggleEnabled={handleToggleEnabled}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthRow;
|
@ -0,0 +1,46 @@
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { ToggleButton } from "@docspace/shared/components/toggle-button";
|
||||
|
||||
import {
|
||||
StyledRowContent,
|
||||
ContentWrapper,
|
||||
FlexWrapper,
|
||||
ToggleButtonWrapper,
|
||||
} from "./RowView.styled";
|
||||
import { RowContentProps } from "./RowView.types";
|
||||
|
||||
export const RowContent = ({
|
||||
sectionWidth,
|
||||
item,
|
||||
|
||||
handleToggleEnabled,
|
||||
}: RowContentProps) => {
|
||||
return (
|
||||
<StyledRowContent sectionWidth={sectionWidth}>
|
||||
<ContentWrapper>
|
||||
<FlexWrapper>
|
||||
<Text
|
||||
fontWeight={600}
|
||||
fontSize="14px"
|
||||
style={{ marginInlineEnd: "8px" }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</FlexWrapper>
|
||||
|
||||
<Text fontWeight={600} fontSize="12px" color="#A3A9AE">
|
||||
{item.description}
|
||||
</Text>
|
||||
</ContentWrapper>
|
||||
|
||||
<ToggleButtonWrapper>
|
||||
<ToggleButton
|
||||
className="toggle toggleButton"
|
||||
id="toggle id"
|
||||
isChecked={item.enabled}
|
||||
onChange={handleToggleEnabled}
|
||||
/>
|
||||
</ToggleButtonWrapper>
|
||||
</StyledRowContent>
|
||||
);
|
||||
};
|
@ -0,0 +1,111 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { RowContainer } from "@docspace/shared/components/row-container";
|
||||
import { RowContent } from "@docspace/shared/components/row-content";
|
||||
import { tablet } from "@docspace/shared/utils/device";
|
||||
|
||||
export const StyledRowContainer = styled(RowContainer)`
|
||||
margin-top: 0px;
|
||||
|
||||
.row-list-item {
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
.row-loader {
|
||||
width: calc(100% - 46px) !important;
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
max-width: 32px;
|
||||
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
.oauth2-row-selected {
|
||||
background: ${(props) =>
|
||||
props.theme.filesSection.rowView.checkedBackground};
|
||||
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
|
||||
@media ${tablet} {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth2-row {
|
||||
margin-top: -3px;
|
||||
padding-top: 3px;
|
||||
|
||||
:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.filesSection.rowView.checkedBackground};
|
||||
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
|
||||
@media ${tablet} {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledRowContent = styled(RowContent)`
|
||||
display: flex;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.rowMainContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mainIcons {
|
||||
min-width: 76px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
`;
|
||||
|
||||
export const ToggleButtonWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
label {
|
||||
margin-top: 1px;
|
||||
position: relative;
|
||||
gap: 0px;
|
||||
|
||||
margin-right: -8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FlexWrapper = styled.div`
|
||||
display: flex;
|
||||
`;
|
@ -0,0 +1,46 @@
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
|
||||
|
||||
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
export interface RowViewProps {
|
||||
items: IClientProps[];
|
||||
sectionWidth: number;
|
||||
viewAs?: ViewAsType;
|
||||
setViewAs?: (value: ViewAsType) => void;
|
||||
selection?: string[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
) => ContextMenuModel[];
|
||||
activeClients?: string[];
|
||||
hasNextPage?: boolean;
|
||||
itemCount?: number;
|
||||
fetchNextClients?: (startIndex: number) => Promise<void>;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RowProps {
|
||||
item: IClientProps;
|
||||
isChecked: boolean;
|
||||
inProgress: boolean;
|
||||
sectionWidth: number;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
) => ContextMenuModel[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RowContentProps {
|
||||
sectionWidth: number;
|
||||
item: IClientProps;
|
||||
isChecked: boolean;
|
||||
inProgress: boolean;
|
||||
handleToggleEnabled: () => void;
|
||||
setSelection?: (clientId: string) => void;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
import { OAuthRow } from "./Row";
|
||||
|
||||
import { RowViewProps } from "./RowView.types";
|
||||
import { StyledRowContainer } from "./RowView.styled";
|
||||
|
||||
const RowView = (props: RowViewProps) => {
|
||||
const {
|
||||
items,
|
||||
sectionWidth,
|
||||
|
||||
changeClientStatus,
|
||||
selection,
|
||||
setSelection,
|
||||
|
||||
activeClients,
|
||||
getContextMenuItems,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextClients,
|
||||
} = props;
|
||||
|
||||
const fetchMoreFiles = React.useCallback(
|
||||
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
|
||||
await fetchNextClients?.(startIndex);
|
||||
},
|
||||
[fetchNextClients],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRowContainer
|
||||
itemHeight={59}
|
||||
filesLength={items.length}
|
||||
fetchMoreFiles={fetchMoreFiles}
|
||||
hasMoreFiles={hasNextPage || false}
|
||||
itemCount={itemCount || 0}
|
||||
useReactWindow
|
||||
onScroll={() => {}}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<OAuthRow
|
||||
key={item.clientId}
|
||||
item={item}
|
||||
isChecked={selection?.includes(item.clientId) || false}
|
||||
inProgress={activeClients?.includes(item.clientId) || false}
|
||||
setSelection={setSelection}
|
||||
changeClientStatus={changeClientStatus}
|
||||
getContextMenuItems={getContextMenuItems}
|
||||
sectionWidth={sectionWidth}
|
||||
/>
|
||||
))}
|
||||
</StyledRowContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
|
||||
const {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
selection,
|
||||
setSelection,
|
||||
changeClientStatus,
|
||||
getContextMenuItems,
|
||||
activeClients,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextClients,
|
||||
} = oauthStore;
|
||||
|
||||
return {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
changeClientStatus,
|
||||
selection,
|
||||
setSelection,
|
||||
activeClients,
|
||||
getContextMenuItems,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextClients,
|
||||
};
|
||||
})(observer(RowView));
|
@ -0,0 +1,67 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TTableColumn, TableHeader } from "@docspace/shared/components/table";
|
||||
|
||||
import { HeaderProps } from "./TableView.types";
|
||||
|
||||
const Header = (props: HeaderProps) => {
|
||||
const { sectionWidth, tableRef, columnStorageName, tagRef } = props;
|
||||
const { t } = useTranslation(["Oauth", "Files", "Webhooks", "Common"]);
|
||||
|
||||
const defaultColumns: TTableColumn[] = [
|
||||
{
|
||||
key: "Name",
|
||||
title: t("Common:Name"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
default: true,
|
||||
active: false,
|
||||
minWidth: 210,
|
||||
},
|
||||
{
|
||||
key: "Creator",
|
||||
title: t("Creator"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
key: "Modified",
|
||||
title: t("Files:ByLastModified"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
key: "Scopes",
|
||||
title: t("Scopes"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
withTagRef: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
key: "State",
|
||||
title: t("Webhooks:State"),
|
||||
enable: true,
|
||||
resizable: false,
|
||||
defaultSize: 64,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<TableHeader
|
||||
containerRef={{ current: tableRef }}
|
||||
columns={defaultColumns}
|
||||
columnStorageName={columnStorageName}
|
||||
tableStorageName={columnStorageName}
|
||||
sectionWidth={sectionWidth}
|
||||
showSettings={false}
|
||||
useReactWindow
|
||||
infoPanelVisible={false}
|
||||
tagRef={tagRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
@ -0,0 +1,131 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TableCell } from "@docspace/shared/components/table";
|
||||
import { Tags } from "@docspace/shared/components/tags";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { ToggleButton } from "@docspace/shared/components/toggle-button";
|
||||
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
|
||||
import { getCookie } from "@docspace/shared/utils/cookie";
|
||||
|
||||
import NameCell from "./columns/name";
|
||||
import CreatorCell from "./columns/creator";
|
||||
|
||||
import { StyledRowWrapper, StyledTableRow } from "./TableView.styled";
|
||||
import { RowProps } from "./TableView.types";
|
||||
|
||||
const Row = (props: RowProps) => {
|
||||
const {
|
||||
item,
|
||||
changeClientStatus,
|
||||
isChecked,
|
||||
inProgress,
|
||||
getContextMenuItems,
|
||||
setSelection,
|
||||
tagCount,
|
||||
} = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation(["OAuth", "Common", "Files"]);
|
||||
|
||||
const editClient = () => {
|
||||
navigate(`${item.clientId}`);
|
||||
};
|
||||
|
||||
const handleToggleEnabled = async () => {
|
||||
if (!changeClientStatus) return;
|
||||
await changeClientStatus(item.clientId, !item.enabled);
|
||||
};
|
||||
|
||||
const handleRowClick = (e: React.MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
if (
|
||||
target.closest(".checkbox") ||
|
||||
target.closest(".table-container_row-checkbox") ||
|
||||
target.closest(".advanced-tag") ||
|
||||
target.closest(".tag") ||
|
||||
e.detail === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
target.closest(".type-combobox") ||
|
||||
target.closest(".table-container_row-context-menu-wrapper") ||
|
||||
target.closest(".toggleButton")
|
||||
) {
|
||||
return setSelection && setSelection("");
|
||||
}
|
||||
|
||||
editClient();
|
||||
};
|
||||
|
||||
const contextOptions = getContextMenuItems?.(t, item);
|
||||
|
||||
const getContextMenuModel = () =>
|
||||
getContextMenuItems ? getContextMenuItems(t, item) : [];
|
||||
|
||||
const locale = getCookie("asc_language");
|
||||
|
||||
const modifiedDate = getCorrectDate(locale || "", item.modifiedOn || "");
|
||||
|
||||
return (
|
||||
<StyledRowWrapper className="handle">
|
||||
<StyledTableRow
|
||||
contextOptions={contextOptions || []}
|
||||
onClick={handleRowClick}
|
||||
getContextModel={getContextMenuModel}
|
||||
>
|
||||
<TableCell className="table-container_file-name-cell">
|
||||
<NameCell
|
||||
name={item.name}
|
||||
icon={item.logo}
|
||||
isChecked={isChecked}
|
||||
inProgress={inProgress}
|
||||
clientId={item.clientId}
|
||||
setSelection={setSelection}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="author-cell">
|
||||
<CreatorCell
|
||||
avatar={item.creatorAvatar || ""}
|
||||
displayName={item.creatorDisplayName || ""}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<Text
|
||||
as="span"
|
||||
fontWeight={400}
|
||||
className="mr-8 textOverflow description-text"
|
||||
>
|
||||
{modifiedDate}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<Text
|
||||
as="span"
|
||||
fontWeight={400}
|
||||
className="mr-8 textOverflow description-text"
|
||||
>
|
||||
<Tags
|
||||
tags={item.scopes}
|
||||
removeTagIcon
|
||||
columnCount={tagCount}
|
||||
onSelectTag={() => {}}
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<ToggleButton
|
||||
className="toggle toggleButton"
|
||||
isChecked={item.enabled}
|
||||
onChange={handleToggleEnabled}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
</StyledRowWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Row;
|
@ -0,0 +1,99 @@
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { TableRow, TableContainer } from "@docspace/shared/components/table";
|
||||
import { Base } from "@docspace/shared/themes";
|
||||
|
||||
export const TableWrapper = styled(TableContainer)`
|
||||
margin-top: 0px;
|
||||
|
||||
.header-container-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-container_header {
|
||||
position: absolute;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRowWrapper = styled.div`
|
||||
display: contents;
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
.table-container_cell {
|
||||
text-overflow: ellipsis;
|
||||
|
||||
padding-inline-end: 8px;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.textOverflow {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
color: ${(props) => props.theme.oauth.list.descriptionColor};
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
display: contents;
|
||||
|
||||
input {
|
||||
position: relative;
|
||||
|
||||
margin-inline-start: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container_row-loader {
|
||||
margin-left: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
:hover {
|
||||
.table-container_cell {
|
||||
cursor: pointer;
|
||||
background: ${(props) =>
|
||||
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
|
||||
|
||||
margin-top: -1px;
|
||||
|
||||
border-top: ${(props) =>
|
||||
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
|
||||
}
|
||||
|
||||
.table-container_file-name-cell {
|
||||
${(props) =>
|
||||
props.theme.interfaceDirection === "rtl"
|
||||
? css`
|
||||
margin-right: -24px;
|
||||
padding-right: 24px;
|
||||
`
|
||||
: css`
|
||||
margin-left: -24px;
|
||||
padding-left: 24px;
|
||||
`}
|
||||
}
|
||||
.table-container_row-context-menu-wrapper {
|
||||
${(props) =>
|
||||
props.theme.interfaceDirection === "rtl"
|
||||
? css`
|
||||
margin-left: -20px;
|
||||
padding-left: 18px;
|
||||
`
|
||||
: css`
|
||||
margin-right: -20px;
|
||||
padding-right: 18px;
|
||||
`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledTableRow.defaultProps = { theme: Base };
|
||||
|
||||
export { StyledRowWrapper, StyledTableRow };
|
@ -0,0 +1,42 @@
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
|
||||
|
||||
export interface TableViewProps {
|
||||
items: IClientProps[];
|
||||
sectionWidth: number;
|
||||
|
||||
userId?: string;
|
||||
selection?: string[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
) => ContextMenuModel[];
|
||||
bufferSelection?: IClientProps | null;
|
||||
activeClients?: string[];
|
||||
hasNextPage?: boolean;
|
||||
itemCount?: number;
|
||||
fetchNextClients?: (startIndex: number) => Promise<void>;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
sectionWidth: number;
|
||||
tableRef: HTMLDivElement | null;
|
||||
columnStorageName: string;
|
||||
tagRef: (node: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
export interface RowProps {
|
||||
item: IClientProps;
|
||||
isChecked: boolean;
|
||||
inProgress: boolean;
|
||||
tagCount: number;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
) => ContextMenuModel[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarRole,
|
||||
AvatarSize,
|
||||
} from "@docspace/shared/components/avatar";
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
width: 16px;
|
||||
margin-inline-end: 4px;
|
||||
max-width: 100%;
|
||||
height: 16px;
|
||||
|
||||
min-width: unset;
|
||||
`;
|
||||
|
||||
interface CreatorCellProps {
|
||||
avatar: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const CreatorCell = ({ avatar, displayName }: CreatorCellProps) => {
|
||||
return (
|
||||
<>
|
||||
<StyledAvatar
|
||||
source={avatar}
|
||||
size={AvatarSize.min}
|
||||
role={AvatarRole.user}
|
||||
className="textOverflow"
|
||||
/>
|
||||
|
||||
<Text
|
||||
className="description-text textOverflow"
|
||||
fontWeight="600"
|
||||
fontSize="13px"
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatorCell;
|
@ -0,0 +1,88 @@
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Checkbox } from "@docspace/shared/components/checkbox";
|
||||
import { TableCell } from "@docspace/shared/components/table";
|
||||
import { Loader, LoaderTypes } from "@docspace/shared/components/loader";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
.table-container_row-checkbox {
|
||||
margin-inline-start: -8px;
|
||||
|
||||
width: 16px;
|
||||
|
||||
${(props) =>
|
||||
props.theme.interfaceDirection === "rtl"
|
||||
? css`
|
||||
padding: 16px 16px 16px 8px;
|
||||
`
|
||||
: css`
|
||||
padding: 16px 8px 16px 16px;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledImage = styled.img`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
interface NameCellProps {
|
||||
name: string;
|
||||
clientId: string;
|
||||
icon?: string;
|
||||
inProgress?: boolean;
|
||||
isChecked?: boolean;
|
||||
setSelection?: (clientId: string) => void;
|
||||
}
|
||||
|
||||
const NameCell = ({
|
||||
name,
|
||||
icon,
|
||||
clientId,
|
||||
inProgress,
|
||||
isChecked,
|
||||
setSelection,
|
||||
}: NameCellProps) => {
|
||||
const onChange = () => {
|
||||
setSelection?.(clientId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{inProgress ? (
|
||||
<Loader
|
||||
className="table-container_row-loader"
|
||||
type={LoaderTypes.oval}
|
||||
size="16px"
|
||||
/>
|
||||
) : (
|
||||
<TableCell
|
||||
className="table-container_element-wrapper"
|
||||
hasAccess
|
||||
checked={isChecked}
|
||||
>
|
||||
<StyledContainer className="table-container_element-container">
|
||||
<div className="table-container_element">
|
||||
{icon && <StyledImage src={icon} alt="App icon" />}
|
||||
</div>
|
||||
<Checkbox
|
||||
className="table-container_row-checkbox"
|
||||
onChange={onChange}
|
||||
isChecked={isChecked}
|
||||
title={name}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<Text title={name} fontWeight="600" fontSize="13px">
|
||||
{name}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameCell;
|
@ -0,0 +1,185 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import elementResizeDetectorMaker from "element-resize-detector";
|
||||
|
||||
import { UserStore } from "@docspace/shared/store/UserStore";
|
||||
import { TableBody } from "@docspace/shared/components/table";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
import Row from "./Row";
|
||||
import Header from "./Header";
|
||||
|
||||
import { TableViewProps } from "./TableView.types";
|
||||
import { TableWrapper } from "./TableView.styled";
|
||||
|
||||
const TABLE_VERSION = "1";
|
||||
const COLUMNS_NAME = `oauthConfigColumnsSize_ver-${TABLE_VERSION}`;
|
||||
|
||||
const elementResizeDetector = elementResizeDetectorMaker({
|
||||
strategy: "scroll",
|
||||
callOnAdd: false,
|
||||
});
|
||||
|
||||
const TableView = ({
|
||||
items,
|
||||
sectionWidth,
|
||||
selection,
|
||||
activeClients,
|
||||
setSelection,
|
||||
getContextMenuItems,
|
||||
changeClientStatus,
|
||||
userId,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextClients,
|
||||
}: TableViewProps) => {
|
||||
const tableRef = React.useRef<HTMLDivElement>(null);
|
||||
const tagRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [tagCount, setTagCount] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (!tagRef?.current) return;
|
||||
|
||||
elementResizeDetector.uninstall(tagRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onResize = React.useCallback(
|
||||
(node: HTMLElement) => {
|
||||
const element = tagRef?.current ? tagRef?.current : node;
|
||||
|
||||
if (element) {
|
||||
const { width } = element.getBoundingClientRect();
|
||||
|
||||
const columns = Math.floor(width / 120);
|
||||
|
||||
if (columns !== tagCount) setTagCount(columns);
|
||||
}
|
||||
},
|
||||
[tagCount],
|
||||
);
|
||||
|
||||
const onSetTagRef = React.useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
if (node) {
|
||||
tagRef.current = node;
|
||||
onResize(node);
|
||||
|
||||
elementResizeDetector.listenTo(node, onResize);
|
||||
}
|
||||
},
|
||||
[onResize],
|
||||
);
|
||||
|
||||
const clickOutside = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target) return;
|
||||
if (
|
||||
target.closest(".checkbox") ||
|
||||
target.closest(".table-container_row-checkbox") ||
|
||||
e.detail === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelection?.("");
|
||||
},
|
||||
[setSelection],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("click", clickOutside);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", clickOutside);
|
||||
};
|
||||
}, [clickOutside, setSelection]);
|
||||
|
||||
const columnStorageName = `${COLUMNS_NAME}=${userId}`;
|
||||
|
||||
const fetchMoreFiles = React.useCallback(
|
||||
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
|
||||
await fetchNextClients?.(startIndex);
|
||||
},
|
||||
[fetchNextClients],
|
||||
);
|
||||
|
||||
return (
|
||||
<TableWrapper forwardedRef={tableRef} useReactWindow>
|
||||
<Header
|
||||
sectionWidth={sectionWidth}
|
||||
tableRef={tableRef.current}
|
||||
columnStorageName={columnStorageName}
|
||||
tagRef={onSetTagRef}
|
||||
/>
|
||||
<TableBody
|
||||
itemHeight={49}
|
||||
useReactWindow
|
||||
columnStorageName={columnStorageName}
|
||||
columnInfoPanelStorageName=" "
|
||||
filesLength={items.length}
|
||||
fetchMoreFiles={fetchMoreFiles}
|
||||
hasMoreFiles={hasNextPage || false}
|
||||
itemCount={itemCount || 0}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Row
|
||||
key={item.clientId}
|
||||
item={item}
|
||||
isChecked={selection?.includes(item.clientId) || false}
|
||||
inProgress={activeClients?.includes(item.clientId) || false}
|
||||
setSelection={setSelection}
|
||||
changeClientStatus={changeClientStatus}
|
||||
getContextMenuItems={getContextMenuItems}
|
||||
tagCount={tagCount}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({
|
||||
userStore,
|
||||
oauthStore,
|
||||
}: {
|
||||
userStore: UserStore;
|
||||
oauthStore: OAuthStoreProps;
|
||||
}) => {
|
||||
const userId = userStore.user?.id;
|
||||
|
||||
const {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
selection,
|
||||
setSelection,
|
||||
setBufferSelection,
|
||||
changeClientStatus,
|
||||
getContextMenuItems,
|
||||
activeClients,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextClients,
|
||||
} = oauthStore;
|
||||
|
||||
return {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
userId,
|
||||
changeClientStatus,
|
||||
selection,
|
||||
setSelection,
|
||||
setBufferSelection,
|
||||
activeClients,
|
||||
getContextMenuItems,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextClients,
|
||||
};
|
||||
},
|
||||
)(observer(TableView));
|
@ -0,0 +1,75 @@
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
|
||||
import { Consumer } from "@docspace/shared/utils/context";
|
||||
|
||||
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
|
||||
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
|
||||
|
||||
import TableView from "./TableView";
|
||||
import RowView from "./RowView";
|
||||
|
||||
import RegisterNewButton from "../RegisterNewButton";
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.description {
|
||||
margin-bottom: 20px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: fit-content;
|
||||
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ListProps {
|
||||
clients: IClientProps[];
|
||||
viewAs: ViewAsType;
|
||||
currentDeviceType: DeviceUnionType;
|
||||
}
|
||||
|
||||
const List = ({ clients, viewAs, currentDeviceType }: ListProps) => {
|
||||
const { t } = useTranslation(["OAuth", "Common"]);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Text
|
||||
fontSize="12px"
|
||||
fontWeight={400}
|
||||
lineHeight="16px"
|
||||
title={t("OAuthAppDescription")}
|
||||
className="description"
|
||||
>
|
||||
{t("OAuthAppDescription")}
|
||||
</Text>
|
||||
<RegisterNewButton currentDeviceType={currentDeviceType} />
|
||||
<Consumer>
|
||||
{(context) =>
|
||||
viewAs === "table" ? (
|
||||
<TableView
|
||||
items={clients || []}
|
||||
sectionWidth={context.sectionWidth || 0}
|
||||
/>
|
||||
) : (
|
||||
<RowView
|
||||
items={clients || []}
|
||||
sectionWidth={context.sectionWidth || 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Consumer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
@ -0,0 +1,354 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
|
||||
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
|
||||
import { SocialButton } from "@docspace/shared/components/social-button";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Textarea } from "@docspace/shared/components/textarea";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
import { Base } from "@docspace/shared/themes";
|
||||
import { generatePKCEPair } from "@docspace/shared/utils/oauth";
|
||||
import { AuthenticationMethod } from "@docspace/shared/enums";
|
||||
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
|
||||
|
||||
import OnlyofficeLight from "PUBLIC_DIR/images/onlyoffice.light.react.svg";
|
||||
import OnlyofficeDark from "PUBLIC_DIR/images/onlyoffice.dark.react.svg";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
`;
|
||||
|
||||
const StyledPreviewContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 152px;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
border: ${(props) => props.theme.oauth.previewDialog.border};
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.social-button {
|
||||
max-width: 226px;
|
||||
|
||||
padding: 11px 16px;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.iconWrapper {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledPreviewContainer.defaultProps = { theme: Base };
|
||||
|
||||
const StyledBlocksContainer = styled.div`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const htmlBlock = `<body>
|
||||
<button id="docspace-button" class="docspace-button">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.89992 18.7913L1.47441 15.2914C0.841864 14.9858 0.841864 14.5136 1.47441 14.2359L4.05959 13.0137L8.87242 15.2914C9.50497 15.5969 10.5225 15.5969 11.1276 15.2914L15.9404 13.0137L18.5256 14.2359C19.1581 14.5414 19.1581 15.0136 18.5256 15.2914L11.1001 18.7913C10.5225 19.069 9.50497 19.069 8.89992 18.7913Z" fill="#FF6F3D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.87586 14.4606L1.47296 10.9566C0.842346 10.6507 0.842346 10.178 1.47296 9.89989L3.99543 8.7041L8.87586 11.0123C9.50647 11.3182 10.5209 11.3182 11.1241 11.0123L16.0046 8.7041L18.527 9.89989C19.1577 10.2058 19.1577 10.6785 18.527 10.9566L11.1241 14.4606C10.4935 14.7665 9.47906 14.7665 8.87586 14.4606Z" fill="#95C038"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.87586 10.1747L1.47296 6.72119C0.842346 6.41969 0.842346 5.95374 1.47296 5.67965L8.87586 2.22612C9.50647 1.92463 10.5209 1.92463 11.1241 2.22612L18.527 5.67965C19.1577 5.98115 19.1577 6.4471 18.527 6.72119L11.1241 10.1747C10.4935 10.4488 9.47906 10.4488 8.87586 10.1747Z" fill="#5DC0E8"/>
|
||||
</svg>
|
||||
|
||||
Sign in with DocSpace
|
||||
</button>
|
||||
</body>`;
|
||||
|
||||
const styleBlock = `<style>
|
||||
.docspace-button {
|
||||
width: auto;
|
||||
padding: 0 20px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
|
||||
height: 40px;
|
||||
|
||||
border: none;
|
||||
stroke: none;
|
||||
background: #ffffff;
|
||||
box-shadow: rgba(0, 0, 0, 0.24) 0px 1px 1px, rgba(0, 0, 0, 0.12) 0px 0px 1px;
|
||||
|
||||
color: rgb(163, 169, 174);
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
user-select: none;
|
||||
|
||||
font-family: Roboto, "Open Sans", sans-serif, Arial;
|
||||
}
|
||||
|
||||
.docspace-button:hover {
|
||||
box-shadow: rgba(0, 0, 0, 0.24) 0px 1px 1px, rgba(0, 0, 0, 0.12) 0px 0px 1px;
|
||||
cursor: pointer;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.docspace-button:active {
|
||||
background-color: #F8F9F9;
|
||||
color: #333333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo-svg {
|
||||
width: 18px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
min-height: 18px;
|
||||
|
||||
margin: 11px 16px;
|
||||
}
|
||||
|
||||
</style>`;
|
||||
|
||||
const linkParams =
|
||||
"width=800,height=800,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no";
|
||||
|
||||
interface PreviewDialogProps {
|
||||
visible: boolean;
|
||||
|
||||
setPreviewDialogVisible?: (value: boolean) => void;
|
||||
client?: IClientProps;
|
||||
}
|
||||
|
||||
const PreviewDialog = ({
|
||||
visible,
|
||||
setPreviewDialogVisible,
|
||||
client,
|
||||
}: PreviewDialogProps) => {
|
||||
const { t } = useTranslation(["OAuth", "Common", "Webhooks"]);
|
||||
const theme = useTheme();
|
||||
|
||||
const [codeVerifier, setCodeVerifier] = React.useState("");
|
||||
const [codeChallenge, setCodeChallenge] = React.useState("");
|
||||
const [state, setState] = React.useState("");
|
||||
|
||||
const onClose = () => setPreviewDialogVisible?.(false);
|
||||
|
||||
const icon = theme.isBase ? OnlyofficeLight : OnlyofficeDark;
|
||||
|
||||
const scopesString = client?.scopes.join(" ");
|
||||
|
||||
const isClientSecretPost = !client?.authenticationMethods.includes(
|
||||
AuthenticationMethod.none,
|
||||
);
|
||||
|
||||
const encodingScopes = encodeURI(scopesString || "");
|
||||
|
||||
const getData = React.useCallback(() => {
|
||||
const { verifier, challenge, state: s } = generatePKCEPair();
|
||||
|
||||
setCodeVerifier(verifier);
|
||||
setCodeChallenge(challenge);
|
||||
setState(s);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
getData();
|
||||
}, [getData]);
|
||||
|
||||
const getLink = () => {
|
||||
return `${
|
||||
window?.ClientConfig?.oauth2.origin
|
||||
}/oauth2/authorize?response_type=code&client_id=${client?.clientId}&redirect_uri=${
|
||||
client?.redirectUris[0]
|
||||
}&scope=${encodingScopes}&state=${state}${
|
||||
isClientSecretPost
|
||||
? ""
|
||||
: `&code_challenge_method=S256&code_challenge=${codeChallenge}`
|
||||
}`;
|
||||
};
|
||||
|
||||
const link = getLink();
|
||||
|
||||
const scriptBlock = `<script>
|
||||
const button = document.getElementById('docspace-button')
|
||||
|
||||
function openOAuthPage() {
|
||||
window.open(
|
||||
"${link}",
|
||||
"login",
|
||||
${linkParams}
|
||||
);
|
||||
}
|
||||
|
||||
button.addEventListener('click', openOAuthPage)
|
||||
</script>`;
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
visible={visible}
|
||||
displayType={ModalDialogType.aside}
|
||||
onClose={onClose}
|
||||
withFooterBorder
|
||||
>
|
||||
<ModalDialog.Header>{t("AuthButton")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<StyledContainer>
|
||||
<StyledPreviewContainer>
|
||||
<SocialButton
|
||||
className="social-button"
|
||||
label={t("SignIn")}
|
||||
IconComponent={icon}
|
||||
onClick={() => {
|
||||
window.open(link, "login", linkParams);
|
||||
}}
|
||||
/>
|
||||
</StyledPreviewContainer>
|
||||
<StyledBlocksContainer>
|
||||
<div className="block-container">
|
||||
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
|
||||
HTML
|
||||
</Text>
|
||||
<Textarea
|
||||
heightTextArea={64}
|
||||
enableCopy
|
||||
isReadOnly
|
||||
isDisabled
|
||||
value={htmlBlock}
|
||||
/>
|
||||
</div>
|
||||
<div className="block-container">
|
||||
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
|
||||
CSS
|
||||
</Text>
|
||||
<Textarea
|
||||
heightTextArea={64}
|
||||
enableCopy
|
||||
isReadOnly
|
||||
isDisabled
|
||||
value={styleBlock}
|
||||
/>
|
||||
</div>
|
||||
<div className="block-container">
|
||||
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
|
||||
JavaScript
|
||||
</Text>
|
||||
<Textarea
|
||||
heightTextArea={64}
|
||||
enableCopy
|
||||
isReadOnly
|
||||
isDisabled
|
||||
value={scriptBlock}
|
||||
/>
|
||||
</div>
|
||||
<div className="block-container">
|
||||
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
|
||||
{t("AuthorizeLink")}
|
||||
</Text>
|
||||
<Textarea
|
||||
heightTextArea={64}
|
||||
enableCopy
|
||||
isReadOnly
|
||||
isDisabled
|
||||
value={link}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="block-container">
|
||||
<Text fontWeight={600} lineHeight="20px" fontSize="13px" noSelect>
|
||||
{t("Webhooks:State")}
|
||||
</Text>
|
||||
<Textarea
|
||||
heightTextArea={64}
|
||||
enableCopy
|
||||
isReadOnly
|
||||
isDisabled
|
||||
value={state}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isClientSecretPost && (
|
||||
<div className="block-container">
|
||||
<Text
|
||||
fontWeight={600}
|
||||
lineHeight="20px"
|
||||
fontSize="13px"
|
||||
noSelect
|
||||
>
|
||||
{t("CodeVerifier")}
|
||||
</Text>
|
||||
<Textarea
|
||||
heightTextArea={64}
|
||||
enableCopy
|
||||
isReadOnly
|
||||
isDisabled
|
||||
value={codeVerifier}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StyledBlocksContainer>
|
||||
</StyledContainer>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<Button
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
label={t("Common:OkButton")}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({
|
||||
oauthStore,
|
||||
settingsStore,
|
||||
}: {
|
||||
settingsStore: SettingsStore;
|
||||
oauthStore: OAuthStoreProps;
|
||||
}) => {
|
||||
const { setPreviewDialogVisible, bufferSelection } = oauthStore;
|
||||
|
||||
const { theme } = settingsStore;
|
||||
|
||||
return {
|
||||
setPreviewDialogVisible,
|
||||
client: bufferSelection,
|
||||
theme,
|
||||
};
|
||||
},
|
||||
)(observer(PreviewDialog));
|
@ -0,0 +1,5 @@
|
||||
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
|
||||
|
||||
export interface RegisterNewButtonProps {
|
||||
currentDeviceType?: DeviceUnionType;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
|
||||
import { RegisterNewButtonProps } from "./RegisterNewButton.types";
|
||||
|
||||
const RegisterNewButton = ({ currentDeviceType }: RegisterNewButtonProps) => {
|
||||
const { t } = useTranslation(["OAuth", "Common"]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onClick = () => {
|
||||
navigate("create");
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="add-button"
|
||||
size={
|
||||
currentDeviceType !== "desktop" ? ButtonSize.normal : ButtonSize.small
|
||||
}
|
||||
label={t("RegisterNewApp")}
|
||||
primary
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterNewButton;
|
@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
|
||||
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
import { TData } from "@docspace/shared/components/toast/Toast.type";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
interface ResetDialogProps {
|
||||
isVisible?: boolean;
|
||||
onClose?: () => void;
|
||||
onReset?: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const ResetDialog = (props: ResetDialogProps) => {
|
||||
const { id } = useParams();
|
||||
|
||||
const { t, ready } = useTranslation(["OAuth", "Common"]);
|
||||
const { isVisible, onClose, onReset } = props;
|
||||
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
|
||||
const onResetClick = async () => {
|
||||
try {
|
||||
setIsRequestRunning(true);
|
||||
if (id) await onReset?.(id);
|
||||
|
||||
setIsRequestRunning(true);
|
||||
onClose?.();
|
||||
} catch (error: unknown) {
|
||||
const e = error as TData;
|
||||
toastr.error(e);
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
isLoading={!ready}
|
||||
visible={isVisible}
|
||||
onClose={onClose}
|
||||
displayType={ModalDialogType.modal}
|
||||
>
|
||||
<ModalDialog.Header>{t("ResetHeader")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<Trans t={t} i18nKey="ResetDescription" ns="OAuth" />
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<Button
|
||||
className="delete-button"
|
||||
key="DeletePortalBtn"
|
||||
label={t("Common:OkButton")}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
primary
|
||||
isLoading={isRequestRunning}
|
||||
onClick={onResetClick}
|
||||
/>
|
||||
<Button
|
||||
className="cancel-button"
|
||||
key="CancelDeleteBtn"
|
||||
label={t("Common:CancelButton")}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
isDisabled={isRequestRunning}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
|
||||
const { setResetDialogVisible, regenerateSecret, resetDialogVisible } =
|
||||
oauthStore;
|
||||
|
||||
const onClose = () => {
|
||||
setResetDialogVisible(false);
|
||||
};
|
||||
|
||||
const onReset = async (id: string) => {
|
||||
await regenerateSecret(id);
|
||||
};
|
||||
|
||||
return { isVisible: resetDialogVisible, onClose, onReset };
|
||||
})(observer(ResetDialog));
|
@ -25,26 +25,29 @@
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
|
||||
import { Tabs } from "@docspace/shared/components/tabs";
|
||||
|
||||
import { Box } from "@docspace/shared/components/box";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { combineUrl } from "@docspace/shared/utils/combineUrl";
|
||||
import config from "PACKAGE_FILE";
|
||||
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Box } from "@docspace/shared/components/box";
|
||||
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
|
||||
import { combineUrl } from "@docspace/shared/utils/combineUrl";
|
||||
|
||||
import JavascriptSDK from "./JavascriptSDK";
|
||||
import Webhooks from "./Webhooks";
|
||||
|
||||
import Api from "./Api";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SSOLoader from "./sub-components/ssoLoader";
|
||||
import PluginSDK from "./PluginSDK";
|
||||
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
|
||||
import OAuth from "./OAuth";
|
||||
|
||||
import SSOLoader from "./sub-components/ssoLoader";
|
||||
|
||||
const DeveloperToolsWrapper = (props) => {
|
||||
const { currentDeviceType } = props;
|
||||
const { currentDeviceType, identityServerEnabled } = props;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@ -57,6 +60,7 @@ const DeveloperToolsWrapper = (props) => {
|
||||
"Settings",
|
||||
"WebPlugins",
|
||||
"Common",
|
||||
"OAuth",
|
||||
]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
@ -95,6 +99,14 @@ const DeveloperToolsWrapper = (props) => {
|
||||
},
|
||||
];
|
||||
|
||||
if (identityServerEnabled) {
|
||||
data.push({
|
||||
id: "oauth",
|
||||
name: t("OAuth:OAuth"),
|
||||
content: <OAuth />,
|
||||
});
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
//await loadBaseInfo();
|
||||
};
|
||||
@ -136,13 +148,16 @@ const DeveloperToolsWrapper = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ setup, settingsStore }) => {
|
||||
export default inject(({ setup, settingsStore, authStore }) => {
|
||||
const { initSettings } = setup;
|
||||
|
||||
const { identityServerEnabled } = authStore.capabilities;
|
||||
|
||||
return {
|
||||
currentDeviceType: settingsStore.currentDeviceType,
|
||||
loadBaseInfo: async () => {
|
||||
await initSettings();
|
||||
},
|
||||
identityServerEnabled,
|
||||
};
|
||||
})(observer(DeveloperToolsWrapper));
|
||||
|
@ -531,6 +531,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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -41,6 +41,8 @@ import FileManagement from "./sub-components/file-management";
|
||||
import InterfaceTheme from "./sub-components/interface-theme";
|
||||
|
||||
import { tablet } from "@docspace/shared/utils";
|
||||
import { DeviceType } from "@docspace/shared/enums";
|
||||
import AuthorizedApps from "./sub-components/authorized-apps";
|
||||
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@ -63,7 +65,13 @@ const StyledTabs = styled(Tabs)`
|
||||
`;
|
||||
|
||||
const SectionBodyContent = (props) => {
|
||||
const { showProfileLoader, profile, currentDeviceType, t } = props;
|
||||
const {
|
||||
showProfileLoader,
|
||||
profile,
|
||||
currentDeviceType,
|
||||
identityServerEnabled,
|
||||
t,
|
||||
} = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const data = [
|
||||
@ -84,6 +92,14 @@ const SectionBodyContent = (props) => {
|
||||
},
|
||||
];
|
||||
|
||||
if (identityServerEnabled) {
|
||||
data.push({
|
||||
id: "authorized-apps",
|
||||
name: t("OAuth:AuthorizedApps"),
|
||||
content: <AuthorizedApps />,
|
||||
});
|
||||
}
|
||||
|
||||
if (!profile?.isVisitor)
|
||||
data.splice(2, 0, {
|
||||
id: "file-management",
|
||||
@ -120,16 +136,21 @@ const SectionBodyContent = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ settingsStore, peopleStore, clientLoadingStore }) => {
|
||||
export default inject(
|
||||
({ settingsStore, peopleStore, clientLoadingStore, authStore }) => {
|
||||
const { showProfileLoader } = clientLoadingStore;
|
||||
const { targetUser: profile } = peopleStore.targetUserStore;
|
||||
|
||||
const { identityServerEnabled } = authStore.capabilities;
|
||||
|
||||
return {
|
||||
profile,
|
||||
currentDeviceType: settingsStore.currentDeviceType,
|
||||
showProfileLoader,
|
||||
identityServerEnabled,
|
||||
};
|
||||
})(
|
||||
},
|
||||
)(
|
||||
observer(
|
||||
withTranslation([
|
||||
"Profile",
|
||||
@ -141,6 +162,7 @@ export default inject(({ settingsStore, peopleStore, clientLoadingStore }) => {
|
||||
"DeleteSelfProfileDialog",
|
||||
"Notifications",
|
||||
"ConnectDialog",
|
||||
"OAuth",
|
||||
])(SectionBodyContent),
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,12 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
export { StyledContainer };
|
@ -0,0 +1,33 @@
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
|
||||
import { DeviceUnionType } from "SRC_DIR/Hooks/useViewEffect";
|
||||
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
export interface AuthorizedAppsProps {
|
||||
consents?: IClientProps[];
|
||||
fetchConsents?: () => Promise<void>;
|
||||
|
||||
viewAs: ViewAsType;
|
||||
setViewAs: (value: string) => void;
|
||||
|
||||
currentDeviceType: DeviceUnionType;
|
||||
|
||||
infoDialogVisible: boolean;
|
||||
fetchScopes?: () => Promise<void>;
|
||||
|
||||
revokeDialogVisible: boolean;
|
||||
setRevokeDialogVisible: (value: boolean) => void;
|
||||
selection: string[];
|
||||
bufferSelection: IClientProps;
|
||||
revokeClient: (value: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RevokeDialogProps {
|
||||
visible: boolean;
|
||||
|
||||
onClose: () => void;
|
||||
selection: string[];
|
||||
bufferSelection: IClientProps;
|
||||
onRevoke: (value: string[]) => Promise<void>;
|
||||
currentDeviceType: DeviceUnionType;
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Consumer } from "@docspace/shared/utils/context";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
|
||||
|
||||
import useViewEffect from "SRC_DIR/Hooks/useViewEffect";
|
||||
import OAuthStore from "SRC_DIR/store/OAuthStore";
|
||||
import InfoDialog from "SRC_DIR/pages/PortalSettings/categories/developer-tools/OAuth/sub-components/InfoDialog";
|
||||
|
||||
import { StyledContainer } from "./AuthorizedApps.styled";
|
||||
import { AuthorizedAppsProps } from "./AuthorizedApps.types";
|
||||
|
||||
import TableView from "./sub-components/TableView";
|
||||
import RowView from "./sub-components/RowView";
|
||||
import RevokeDialog from "./sub-components/RevokeDialog";
|
||||
import EmptyScreen from "./sub-components/EmptyScreen";
|
||||
|
||||
const AuthorizedApps = ({
|
||||
consents,
|
||||
fetchConsents,
|
||||
viewAs,
|
||||
setViewAs,
|
||||
currentDeviceType,
|
||||
infoDialogVisible,
|
||||
fetchScopes,
|
||||
revokeDialogVisible,
|
||||
setRevokeDialogVisible,
|
||||
selection,
|
||||
bufferSelection,
|
||||
revokeClient,
|
||||
}: AuthorizedAppsProps) => {
|
||||
const { t } = useTranslation(["OAuth"]);
|
||||
|
||||
const getConsentList = React.useCallback(async () => {
|
||||
fetchScopes?.();
|
||||
await fetchConsents?.();
|
||||
}, [fetchConsents, fetchScopes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (consents?.length) return;
|
||||
|
||||
getConsentList();
|
||||
}, [consents?.length, getConsentList]);
|
||||
|
||||
useViewEffect({
|
||||
view: viewAs,
|
||||
setView: setViewAs,
|
||||
currentDeviceType,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{consents && consents?.length > 0 ? (
|
||||
<>
|
||||
<Text fontSize="12px" fontWeight="400" lineHeight="16px">
|
||||
{t("ProfileDescription")}
|
||||
</Text>
|
||||
|
||||
<Consumer>
|
||||
{(context) =>
|
||||
viewAs === "table" ? (
|
||||
<TableView
|
||||
items={consents || []}
|
||||
sectionWidth={context.sectionWidth || 0}
|
||||
/>
|
||||
) : (
|
||||
<RowView
|
||||
items={consents || []}
|
||||
sectionWidth={context.sectionWidth || 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Consumer>
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen t={t} />
|
||||
)}
|
||||
{infoDialogVisible && (
|
||||
<InfoDialog visible={infoDialogVisible} isProfile />
|
||||
)}
|
||||
{revokeDialogVisible && (
|
||||
<RevokeDialog
|
||||
visible={revokeDialogVisible}
|
||||
onClose={() => setRevokeDialogVisible(false)}
|
||||
currentDeviceType={currentDeviceType}
|
||||
onRevoke={revokeClient}
|
||||
selection={selection}
|
||||
bufferSelection={bufferSelection}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({
|
||||
oauthStore,
|
||||
settingsStore,
|
||||
}: {
|
||||
oauthStore: OAuthStore;
|
||||
settingsStore: SettingsStore;
|
||||
}) => {
|
||||
const {
|
||||
consents,
|
||||
fetchConsents,
|
||||
fetchScopes,
|
||||
viewAs,
|
||||
setViewAs,
|
||||
infoDialogVisible,
|
||||
revokeDialogVisible,
|
||||
setRevokeDialogVisible,
|
||||
|
||||
selection,
|
||||
bufferSelection,
|
||||
revokeClient,
|
||||
} = oauthStore;
|
||||
|
||||
const { currentDeviceType } = settingsStore;
|
||||
|
||||
return {
|
||||
consents,
|
||||
fetchConsents,
|
||||
viewAs,
|
||||
setViewAs,
|
||||
currentDeviceType,
|
||||
infoDialogVisible,
|
||||
fetchScopes,
|
||||
revokeDialogVisible,
|
||||
setRevokeDialogVisible,
|
||||
selection,
|
||||
bufferSelection,
|
||||
revokeClient,
|
||||
};
|
||||
},
|
||||
)(observer(AuthorizedApps));
|
@ -0,0 +1,17 @@
|
||||
import { EmptyScreenContainer } from "@docspace/shared/components/empty-screen-container";
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
|
||||
import EmptyScreenPersonsSvgUrl from "PUBLIC_DIR/images/empty_screen_oauth.svg?url";
|
||||
|
||||
const EmptyScreen = ({ t }: { t: TTranslation }) => {
|
||||
return (
|
||||
<EmptyScreenContainer
|
||||
imageSrc={EmptyScreenPersonsSvgUrl}
|
||||
imageAlt="Empty apps list"
|
||||
headerText={t("NoAuthorizedApps")}
|
||||
descriptionText={t("ProfileDescription")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyScreen;
|
@ -0,0 +1,104 @@
|
||||
import React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
import {
|
||||
ModalDialog,
|
||||
ModalDialogType,
|
||||
} from "@docspace/shared/components/modal-dialog";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
|
||||
import { RevokeDialogProps } from "../AuthorizedApps.types";
|
||||
|
||||
const RevokeDialog = ({
|
||||
visible,
|
||||
onRevoke,
|
||||
onClose,
|
||||
selection,
|
||||
bufferSelection,
|
||||
currentDeviceType,
|
||||
}: RevokeDialogProps) => {
|
||||
const { t } = useTranslation(["OAuth", "Common"]);
|
||||
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
|
||||
const isMobile = currentDeviceType === "mobile";
|
||||
const isGroup = selection.length > 1;
|
||||
const name = bufferSelection?.name;
|
||||
|
||||
const firstDesc = isGroup ? (
|
||||
t("RevokeConsentDescriptionGroup")
|
||||
) : (
|
||||
<Trans t={t} i18nKey="RevokeConsentDescription" ns="OAuth">
|
||||
Once you revoke the consent to use the ONLYOFFICE DocSpace auth data in
|
||||
the service {{ name }}, ONLYOFFICE DocSpace will automatically stop
|
||||
logging into {{ name }}. Your account in {{ name }} will not be deleted.
|
||||
</Trans>
|
||||
);
|
||||
const secondDesc = isGroup ? (
|
||||
t("RevokeConsentLogin")
|
||||
) : (
|
||||
<Trans t={t} i18nKey="RevokeConsentLogin" ns="OAuth">
|
||||
If you want to renew an automatic login into {{ name }} using ONLYOFFICE
|
||||
DocSpace, you will be asked to grant access to your DocSpace account data.
|
||||
</Trans>
|
||||
);
|
||||
|
||||
const onRevokeAction = async () => {
|
||||
if (isRequestRunning) return;
|
||||
|
||||
setIsRequestRunning(true);
|
||||
|
||||
if (isGroup) {
|
||||
await onRevoke(selection);
|
||||
} else {
|
||||
await onRevoke([bufferSelection.clientId]);
|
||||
}
|
||||
|
||||
setIsRequestRunning(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCloseAction = () => {
|
||||
if (isRequestRunning) return;
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
visible={visible}
|
||||
isLarge
|
||||
autoMaxHeight
|
||||
withFooterBorder={isMobile}
|
||||
onClose={onCloseAction}
|
||||
displayType={ModalDialogType.modal}
|
||||
>
|
||||
<ModalDialog.Header>{t("RevokeConsent")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<Text style={{ marginBottom: "16px" }}>{firstDesc}</Text>
|
||||
|
||||
<Text>{secondDesc}</Text>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<Button
|
||||
label={t("Revoke")}
|
||||
primary
|
||||
scale={isMobile}
|
||||
size={ButtonSize.normal}
|
||||
isLoading={isRequestRunning}
|
||||
onClick={onRevokeAction}
|
||||
/>
|
||||
<Button
|
||||
label={t("Common:CancelButton")}
|
||||
scale={isMobile}
|
||||
size={ButtonSize.normal}
|
||||
isDisabled={isRequestRunning}
|
||||
onClick={onCloseAction}
|
||||
/>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RevokeDialog;
|
@ -0,0 +1,48 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Row } from "@docspace/shared/components/row";
|
||||
|
||||
import { RowContent } from "./RowContent";
|
||||
import { RowProps } from "./RowView.types";
|
||||
|
||||
export const OAuthRow = (props: RowProps) => {
|
||||
const {
|
||||
item,
|
||||
sectionWidth,
|
||||
|
||||
isChecked,
|
||||
inProgress,
|
||||
getContextMenuItems,
|
||||
setSelection,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation(["OAuth", "Common", "Files"]);
|
||||
|
||||
const contextOptions = getContextMenuItems?.(t, item, false, false) || [];
|
||||
|
||||
const element = (
|
||||
<img style={{ borderRadius: "3px" }} src={item.logo} alt="App logo" />
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
key={item.clientId}
|
||||
contextOptions={contextOptions}
|
||||
element={element}
|
||||
mode="modern"
|
||||
checked={isChecked}
|
||||
inProgress={inProgress}
|
||||
onSelect={() => setSelection && setSelection(item.clientId)}
|
||||
onRowClick={() => {}}
|
||||
className={`oauth2-row${isChecked ? " oauth2-row-selected" : ""}`}
|
||||
>
|
||||
<RowContent
|
||||
sectionWidth={sectionWidth}
|
||||
item={item}
|
||||
isChecked={isChecked}
|
||||
inProgress={inProgress}
|
||||
setSelection={setSelection}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
|
||||
|
||||
import {
|
||||
StyledRowContent,
|
||||
ContentWrapper,
|
||||
FlexWrapper,
|
||||
} from "./RowView.styled";
|
||||
import { RowContentProps } from "./RowView.types";
|
||||
|
||||
export const RowContent = ({ sectionWidth, item }: RowContentProps) => {
|
||||
return (
|
||||
<StyledRowContent sectionWidth={sectionWidth}>
|
||||
<ContentWrapper>
|
||||
<FlexWrapper>
|
||||
<Text
|
||||
fontWeight={600}
|
||||
fontSize="14px"
|
||||
style={{ marginInlineEnd: "8px" }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</FlexWrapper>
|
||||
|
||||
<Text fontWeight={600} fontSize="12px" color="#A3A9AE">
|
||||
<Link
|
||||
color="#A3A9AE"
|
||||
href={item.websiteUrl}
|
||||
type={LinkType.page}
|
||||
target={LinkTarget.blank}
|
||||
isHovered
|
||||
>
|
||||
{item.websiteUrl}
|
||||
</Link>
|
||||
</Text>
|
||||
</ContentWrapper>
|
||||
{null}
|
||||
</StyledRowContent>
|
||||
);
|
||||
};
|
@ -0,0 +1,111 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { RowContainer } from "@docspace/shared/components/row-container";
|
||||
import { RowContent } from "@docspace/shared/components/row-content";
|
||||
import { tablet } from "@docspace/shared/utils/device";
|
||||
|
||||
export const StyledRowContainer = styled(RowContainer)`
|
||||
margin-top: 0px;
|
||||
|
||||
.row-list-item {
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
.row-loader {
|
||||
width: calc(100% - 46px) !important;
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
max-width: 32px;
|
||||
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
.oauth2-row-selected {
|
||||
background: ${(props) =>
|
||||
props.theme.filesSection.rowView.checkedBackground};
|
||||
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
|
||||
@media ${tablet} {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth2-row {
|
||||
margin-top: -3px;
|
||||
padding-top: 3px;
|
||||
|
||||
:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.filesSection.rowView.checkedBackground};
|
||||
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
|
||||
@media ${tablet} {
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledRowContent = styled(RowContent)`
|
||||
display: flex;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.rowMainContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mainIcons {
|
||||
min-width: 76px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-items: center;
|
||||
`;
|
||||
|
||||
export const ToggleButtonWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
label {
|
||||
margin-top: 1px;
|
||||
position: relative;
|
||||
gap: 0px;
|
||||
|
||||
margin-right: -8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FlexWrapper = styled.div`
|
||||
display: flex;
|
||||
`;
|
@ -0,0 +1,46 @@
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
|
||||
export interface RowViewProps {
|
||||
items: IClientProps[];
|
||||
sectionWidth: number;
|
||||
|
||||
selection?: string[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo: boolean,
|
||||
isSettings: boolean,
|
||||
) => ContextMenuModel[];
|
||||
activeClients?: string[];
|
||||
hasNextPage?: boolean;
|
||||
itemCount?: number;
|
||||
fetchNextConsents?: (startIndex: number) => Promise<void>;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RowProps {
|
||||
item: IClientProps;
|
||||
isChecked: boolean;
|
||||
inProgress: boolean;
|
||||
sectionWidth: number;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo: boolean,
|
||||
isSettings: boolean,
|
||||
) => ContextMenuModel[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RowContentProps {
|
||||
sectionWidth: number;
|
||||
item: IClientProps;
|
||||
isChecked: boolean;
|
||||
inProgress: boolean;
|
||||
|
||||
setSelection?: (clientId: string) => void;
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
import { OAuthRow } from "./Row";
|
||||
|
||||
import { RowViewProps } from "./RowView.types";
|
||||
import { StyledRowContainer } from "./RowView.styled";
|
||||
|
||||
const RowView = (props: RowViewProps) => {
|
||||
const {
|
||||
items,
|
||||
sectionWidth,
|
||||
|
||||
changeClientStatus,
|
||||
selection,
|
||||
setSelection,
|
||||
|
||||
activeClients,
|
||||
getContextMenuItems,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextConsents,
|
||||
} = props;
|
||||
|
||||
const fetchMoreFiles = React.useCallback(
|
||||
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
|
||||
await fetchNextConsents?.(startIndex);
|
||||
},
|
||||
[fetchNextConsents],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRowContainer
|
||||
itemHeight={59}
|
||||
filesLength={items.length}
|
||||
fetchMoreFiles={fetchMoreFiles}
|
||||
hasMoreFiles={hasNextPage || false}
|
||||
itemCount={itemCount || 0}
|
||||
useReactWindow
|
||||
onScroll={() => {}}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<OAuthRow
|
||||
key={item.clientId}
|
||||
item={item}
|
||||
isChecked={selection?.includes(item.clientId) || false}
|
||||
inProgress={activeClients?.includes(item.clientId) || false}
|
||||
setSelection={setSelection}
|
||||
changeClientStatus={changeClientStatus}
|
||||
getContextMenuItems={getContextMenuItems}
|
||||
sectionWidth={sectionWidth}
|
||||
/>
|
||||
))}
|
||||
</StyledRowContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
|
||||
const {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
selection,
|
||||
setSelection,
|
||||
changeClientStatus,
|
||||
getContextMenuItems,
|
||||
activeClients,
|
||||
consentHasNextPage,
|
||||
consentItemCount,
|
||||
fetchNextConsents,
|
||||
} = oauthStore;
|
||||
|
||||
return {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
changeClientStatus,
|
||||
selection,
|
||||
setSelection,
|
||||
activeClients,
|
||||
getContextMenuItems,
|
||||
hasNextPage: consentHasNextPage,
|
||||
itemCount: consentItemCount,
|
||||
fetchNextConsents,
|
||||
};
|
||||
})(observer(RowView));
|
@ -0,0 +1,51 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TTableColumn, TableHeader } from "@docspace/shared/components/table";
|
||||
|
||||
import { HeaderProps } from "./TableView.types";
|
||||
|
||||
const Header = (props: HeaderProps) => {
|
||||
const { sectionWidth, tableRef, columnStorageName, tagRef } = props;
|
||||
const { t } = useTranslation(["Common", "OAuth"]);
|
||||
|
||||
const defaultColumns: TTableColumn[] = [
|
||||
{
|
||||
key: "App",
|
||||
title: t("Apps"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
default: true,
|
||||
active: false,
|
||||
minWidth: 210,
|
||||
},
|
||||
{
|
||||
key: "Website",
|
||||
title: t("Website"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
key: "Access granted",
|
||||
title: t("OAuth:AccessGranted"),
|
||||
resizable: true,
|
||||
enable: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<TableHeader
|
||||
containerRef={{ current: tableRef }}
|
||||
columns={defaultColumns}
|
||||
columnStorageName={columnStorageName}
|
||||
sectionWidth={sectionWidth}
|
||||
showSettings={false}
|
||||
useReactWindow
|
||||
infoPanelVisible={false}
|
||||
tagRef={tagRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
@ -0,0 +1,86 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TableCell } from "@docspace/shared/components/table";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
|
||||
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
|
||||
|
||||
import { getCookie } from "@docspace/shared/utils/cookie";
|
||||
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
|
||||
|
||||
import NameCell from "./columns/name";
|
||||
|
||||
import { StyledRowWrapper, StyledTableRow } from "./TableView.styled";
|
||||
import { RowProps } from "./TableView.types";
|
||||
|
||||
const Row = (props: RowProps) => {
|
||||
const {
|
||||
item,
|
||||
|
||||
isChecked,
|
||||
inProgress,
|
||||
getContextMenuItems,
|
||||
setSelection,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation(["OAuth", "Common", "Files"]);
|
||||
|
||||
const contextOptions = getContextMenuItems?.(t, item, false, false);
|
||||
|
||||
const locale = getCookie("asc_language");
|
||||
|
||||
const modifiedDate = getCorrectDate(locale || "", item.modifiedOn || "");
|
||||
|
||||
const getContextMenuModel = () =>
|
||||
getContextMenuItems ? getContextMenuItems(t, item, false, false) : [];
|
||||
|
||||
return (
|
||||
<StyledRowWrapper className="handle">
|
||||
<StyledTableRow
|
||||
contextOptions={contextOptions}
|
||||
getContextModel={getContextMenuModel}
|
||||
>
|
||||
<TableCell className="table-container_file-name-cell">
|
||||
<NameCell
|
||||
name={item.name}
|
||||
icon={item.logo}
|
||||
isChecked={isChecked}
|
||||
inProgress={inProgress}
|
||||
clientId={item.clientId}
|
||||
setSelection={setSelection}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="">
|
||||
<Text
|
||||
as="span"
|
||||
fontWeight={400}
|
||||
className="mr-8 textOverflow description-text"
|
||||
>
|
||||
<Link
|
||||
className="description-text"
|
||||
href={item.websiteUrl}
|
||||
type={LinkType.action}
|
||||
target={LinkTarget.blank}
|
||||
isHovered
|
||||
>
|
||||
{item.websiteUrl}
|
||||
</Link>
|
||||
</Text>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="">
|
||||
<Text
|
||||
as="span"
|
||||
fontWeight={400}
|
||||
className="mr-8 textOverflow description-text"
|
||||
>
|
||||
{modifiedDate}
|
||||
</Text>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
</StyledRowWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Row;
|
@ -0,0 +1,99 @@
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { TableRow, TableContainer } from "@docspace/shared/components/table";
|
||||
import { Base } from "@docspace/shared/themes";
|
||||
|
||||
export const TableWrapper = styled(TableContainer)`
|
||||
margin-top: 0px;
|
||||
|
||||
.header-container-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-container_header {
|
||||
position: absolute;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRowWrapper = styled.div`
|
||||
display: contents;
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
.table-container_cell {
|
||||
text-overflow: ellipsis;
|
||||
|
||||
padding-inline-end: 8px;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
.textOverflow {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
color: ${(props) => props.theme.oauth.list.descriptionColor};
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
display: contents;
|
||||
|
||||
input {
|
||||
position: relative;
|
||||
|
||||
margin-inline-start: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container_row-loader {
|
||||
margin-left: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
:hover {
|
||||
.table-container_cell {
|
||||
cursor: pointer;
|
||||
background: ${(props) =>
|
||||
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
|
||||
|
||||
margin-top: -1px;
|
||||
|
||||
border-top: ${(props) =>
|
||||
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
|
||||
}
|
||||
|
||||
.table-container_file-name-cell {
|
||||
${(props) =>
|
||||
props.theme.interfaceDirection === "rtl"
|
||||
? css`
|
||||
margin-right: -24px;
|
||||
padding-right: 24px;
|
||||
`
|
||||
: css`
|
||||
margin-left: -24px;
|
||||
padding-left: 24px;
|
||||
`}
|
||||
}
|
||||
.table-container_row-context-menu-wrapper {
|
||||
${(props) =>
|
||||
props.theme.interfaceDirection === "rtl"
|
||||
? css`
|
||||
margin-left: -20px;
|
||||
padding-left: 18px;
|
||||
`
|
||||
: css`
|
||||
margin-right: -20px;
|
||||
padding-right: 18px;
|
||||
`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledTableRow.defaultProps = { theme: Base };
|
||||
|
||||
export { StyledRowWrapper, StyledTableRow };
|
@ -0,0 +1,45 @@
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
|
||||
import { TTranslation } from "@docspace/shared/types";
|
||||
|
||||
export interface TableViewProps {
|
||||
items: IClientProps[];
|
||||
sectionWidth: number;
|
||||
|
||||
userId?: string;
|
||||
selection?: string[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo: boolean,
|
||||
isSettings: boolean,
|
||||
) => ContextMenuModel[];
|
||||
bufferSelection?: IClientProps | null;
|
||||
activeClients?: string[];
|
||||
hasNextPage?: boolean;
|
||||
itemCount?: number;
|
||||
fetchNextConsents?: (startIndex: number) => Promise<void>;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
sectionWidth: number;
|
||||
tableRef: HTMLDivElement | null;
|
||||
columnStorageName: string;
|
||||
tagRef?: (node: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
export interface RowProps {
|
||||
item: IClientProps;
|
||||
isChecked: boolean;
|
||||
inProgress: boolean;
|
||||
getContextMenuItems?: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo: boolean,
|
||||
isSettings: boolean,
|
||||
) => ContextMenuModel[];
|
||||
setSelection?: (clientId: string) => void;
|
||||
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Checkbox } from "@docspace/shared/components/checkbox";
|
||||
import { TableCell } from "@docspace/shared/components/table";
|
||||
import { Loader, LoaderTypes } from "@docspace/shared/components/loader";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
.table-container_row-checkbox {
|
||||
margin-inline-start: -8px;
|
||||
|
||||
width: 16px;
|
||||
|
||||
${(props) =>
|
||||
props.theme.interfaceDirection === "rtl"
|
||||
? css`
|
||||
padding: 16px 16px 16px 8px;
|
||||
`
|
||||
: css`
|
||||
padding: 16px 8px 16px 16px;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledImage = styled.img`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
interface NameCellProps {
|
||||
name: string;
|
||||
clientId: string;
|
||||
icon?: string;
|
||||
inProgress?: boolean;
|
||||
isChecked?: boolean;
|
||||
setSelection?: (clientId: string) => void;
|
||||
}
|
||||
|
||||
const NameCell = ({
|
||||
name,
|
||||
icon,
|
||||
clientId,
|
||||
inProgress,
|
||||
isChecked,
|
||||
setSelection,
|
||||
}: NameCellProps) => {
|
||||
const onChange = () => {
|
||||
setSelection?.(clientId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{inProgress ? (
|
||||
<Loader
|
||||
className="table-container_row-loader"
|
||||
type={LoaderTypes.oval}
|
||||
size="16px"
|
||||
/>
|
||||
) : (
|
||||
<TableCell
|
||||
className="table-container_element-wrapper"
|
||||
hasAccess
|
||||
checked={isChecked}
|
||||
>
|
||||
<StyledContainer className="table-container_element-container">
|
||||
<div className="table-container_element">
|
||||
{icon && <StyledImage src={icon} alt="App icon" />}
|
||||
</div>
|
||||
<Checkbox
|
||||
className="table-container_row-checkbox"
|
||||
onChange={onChange}
|
||||
isChecked={isChecked}
|
||||
title={name}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<Text title={name} fontWeight="600" fontSize="13px">
|
||||
{name}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameCell;
|
@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { TableBody } from "@docspace/shared/components/table";
|
||||
import { UserStore } from "@docspace/shared/store/UserStore";
|
||||
|
||||
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
|
||||
|
||||
import Row from "./Row";
|
||||
import Header from "./Header";
|
||||
|
||||
import { TableViewProps } from "./TableView.types";
|
||||
import { TableWrapper } from "./TableView.styled";
|
||||
|
||||
const TABLE_VERSION = "1";
|
||||
const COLUMNS_SIZE = `consentColumnsSize_ver-${TABLE_VERSION}`;
|
||||
|
||||
const TableView = ({
|
||||
items,
|
||||
sectionWidth,
|
||||
selection,
|
||||
activeClients,
|
||||
setSelection,
|
||||
getContextMenuItems,
|
||||
changeClientStatus,
|
||||
userId,
|
||||
hasNextPage,
|
||||
itemCount,
|
||||
fetchNextConsents,
|
||||
}: TableViewProps) => {
|
||||
const tableRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const clickOutside = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest(".checkbox") ||
|
||||
target.closest(".table-container_row-checkbox") ||
|
||||
e.detail === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelection?.("");
|
||||
},
|
||||
[setSelection],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("click", clickOutside);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", clickOutside);
|
||||
};
|
||||
}, [clickOutside, setSelection]);
|
||||
|
||||
const columnStorageName = `${COLUMNS_SIZE}=${userId}`;
|
||||
|
||||
const fetchMoreFiles = React.useCallback(
|
||||
async ({ startIndex }: { startIndex: number; stopIndex: number }) => {
|
||||
await fetchNextConsents?.(startIndex);
|
||||
},
|
||||
[fetchNextConsents],
|
||||
);
|
||||
|
||||
return (
|
||||
<TableWrapper forwardedRef={tableRef} useReactWindow>
|
||||
<Header
|
||||
sectionWidth={sectionWidth}
|
||||
tableRef={tableRef.current}
|
||||
columnStorageName={columnStorageName}
|
||||
/>
|
||||
<TableBody
|
||||
itemHeight={49}
|
||||
useReactWindow
|
||||
columnStorageName={columnStorageName}
|
||||
columnInfoPanelStorageName=" "
|
||||
filesLength={items.length}
|
||||
fetchMoreFiles={fetchMoreFiles}
|
||||
hasMoreFiles={hasNextPage || false}
|
||||
itemCount={itemCount || 0}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Row
|
||||
key={item.clientId}
|
||||
item={item}
|
||||
isChecked={selection?.includes(item.clientId) || false}
|
||||
inProgress={activeClients?.includes(item.clientId) || false}
|
||||
setSelection={setSelection}
|
||||
changeClientStatus={changeClientStatus}
|
||||
getContextMenuItems={getContextMenuItems}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({
|
||||
userStore,
|
||||
oauthStore,
|
||||
}: {
|
||||
userStore: UserStore;
|
||||
oauthStore: OAuthStoreProps;
|
||||
}) => {
|
||||
const userId = userStore.user?.id;
|
||||
|
||||
const {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
selection,
|
||||
setSelection,
|
||||
setBufferSelection,
|
||||
changeClientStatus,
|
||||
getContextMenuItems,
|
||||
activeClients,
|
||||
consentHasNextPage,
|
||||
consentItemCount,
|
||||
fetchNextConsents,
|
||||
} = oauthStore;
|
||||
|
||||
return {
|
||||
viewAs,
|
||||
setViewAs,
|
||||
userId,
|
||||
changeClientStatus,
|
||||
selection,
|
||||
setSelection,
|
||||
setBufferSelection,
|
||||
activeClients,
|
||||
getContextMenuItems,
|
||||
hasNextPage: consentHasNextPage,
|
||||
itemCount: consentItemCount,
|
||||
fetchNextConsents,
|
||||
};
|
||||
},
|
||||
)(observer(TableView));
|
@ -81,6 +81,14 @@ const generalRoutes = [
|
||||
</PrivateRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "authorized-apps",
|
||||
element: (
|
||||
<PrivateRoute>
|
||||
<Profile />
|
||||
</PrivateRoute>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@ -357,6 +357,24 @@ const Viewer = loadable(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const OAuthCreatePage = loadable(() =>
|
||||
componentLoader(
|
||||
() =>
|
||||
import(
|
||||
"../pages/PortalSettings/categories/developer-tools/OAuth/OAuthCreatePage"
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const OAuthEditPage = loadable(() =>
|
||||
componentLoader(
|
||||
() =>
|
||||
import(
|
||||
"../pages/PortalSettings/categories/developer-tools/OAuth/OAuthEditPage"
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const PortalSettingsRoutes = {
|
||||
path: "portal-settings/",
|
||||
element: (
|
||||
@ -596,6 +614,18 @@ const PortalSettingsRoutes = {
|
||||
path: "developer-tools/webhooks/:id/:eventId",
|
||||
element: <WebhookDetails />,
|
||||
},
|
||||
{
|
||||
path: "developer-tools/oauth",
|
||||
element: <DeveloperTools />,
|
||||
},
|
||||
{
|
||||
path: "developer-tools/oauth/create",
|
||||
element: <OAuthCreatePage />,
|
||||
},
|
||||
{
|
||||
path: "developer-tools/oauth/:id",
|
||||
element: <OAuthEditPage />,
|
||||
},
|
||||
{
|
||||
path: "backup",
|
||||
element: <Navigate to="backup/data-backup" replace />,
|
||||
|
0
packages/client/src/store/FilesActiveItemsStore.ts
Normal file
0
packages/client/src/store/FilesActiveItemsStore.ts
Normal file
0
packages/client/src/store/FilesFilterStore.ts
Normal file
0
packages/client/src/store/FilesFilterStore.ts
Normal file
957
packages/client/src/store/FilesListStore.ts
Normal file
957
packages/client/src/store/FilesListStore.ts
Normal file
@ -0,0 +1,957 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import axios from "axios";
|
||||
|
||||
import api from "@docspace/shared/api";
|
||||
|
||||
import { TFile, TFolder } from "@docspace/shared/api/files/types";
|
||||
import FilesFilter, {
|
||||
TSortBy,
|
||||
TSortOrder,
|
||||
} from "@docspace/shared/api/files/filter";
|
||||
|
||||
import { TRoom } from "@docspace/shared/api/rooms/types";
|
||||
import RoomsFilter from "@docspace/shared/api/rooms/filter";
|
||||
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
|
||||
import { ROOMS_PROVIDER_TYPE_NAME } from "@docspace/shared/constants";
|
||||
|
||||
import { isDesktop } from "@docspace/shared/utils";
|
||||
import { getDaysRemaining, isPublicRoom } from "@docspace/shared/utils/common";
|
||||
|
||||
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
|
||||
import { UserStore } from "@docspace/shared/store/UserStore";
|
||||
|
||||
import {
|
||||
FileStatus,
|
||||
FilterKeys,
|
||||
FilterType,
|
||||
FolderType,
|
||||
RoomSearchArea,
|
||||
RoomsProviderType,
|
||||
RoomsType,
|
||||
ShareAccessRights,
|
||||
} from "@docspace/shared/enums";
|
||||
|
||||
import {
|
||||
getCategoryTypeByFolderType,
|
||||
getCategoryUrl,
|
||||
} from "SRC_DIR/helpers/utils";
|
||||
import { CategoryType } from "SRC_DIR/helpers/constants";
|
||||
|
||||
import FilesStore from "./FilesStore";
|
||||
import SelectedFolderStore from "./SelectedFolderStore";
|
||||
import TreeFoldersStore from "./TreeFoldersStore";
|
||||
import ClientLoadingStore from "./ClientLoadingStore";
|
||||
import PublicRoomStore from "./PublicRoomStore";
|
||||
import InfoPanelStore from "./InfoPanelStore";
|
||||
import PluginStore from "./PluginStore";
|
||||
import FilesSettingsStore from "./FilesSettingsStore";
|
||||
import ThirdPartyStore from "./ThirdPartyStore";
|
||||
|
||||
let requestCounter = 0;
|
||||
|
||||
const NotFoundHttpCode = 404;
|
||||
const ForbiddenHttpCode = 403;
|
||||
const PaymentRequiredHttpCode = 402;
|
||||
const UnauthorizedHttpCode = 401;
|
||||
|
||||
class FilesListStore {
|
||||
files: Map<string | number, TFile> = new Map();
|
||||
|
||||
folders: Map<string | number, TFolder | TRoom> = new Map();
|
||||
|
||||
isEmptyPage: boolean = false;
|
||||
|
||||
roomsController = new AbortController();
|
||||
|
||||
filesController = new AbortController();
|
||||
|
||||
constructor(
|
||||
private settingsStore: SettingsStore,
|
||||
private selectedFolderStore: SelectedFolderStore,
|
||||
private treeFoldersStore: TreeFoldersStore,
|
||||
private clientLoadingStore: ClientLoadingStore,
|
||||
private userStore: UserStore,
|
||||
private publicRoomStore: PublicRoomStore,
|
||||
private infoPanelStore: InfoPanelStore,
|
||||
private pluginStore: PluginStore,
|
||||
private filesSettingsStore: FilesSettingsStore,
|
||||
private thirdPartyStore: ThirdPartyStore,
|
||||
|
||||
private filesStore: FilesStore,
|
||||
) {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setIsEmptyPage = (isEmptyPage: boolean) => {
|
||||
this.isEmptyPage = isEmptyPage;
|
||||
};
|
||||
|
||||
setFile = (file: TFile) => {
|
||||
if (!this.files.has(file.id)) return;
|
||||
|
||||
this.files.set(file.id, file);
|
||||
this.filesStore.createThumbnail(file);
|
||||
};
|
||||
|
||||
setFiles = (files: TFile[]) => {
|
||||
const { socketHelper } = this.settingsStore;
|
||||
|
||||
if (this.files.size > 0) {
|
||||
const roomParts = Array.from(this.files.keys()).map((k) => `FILE-${k}`);
|
||||
|
||||
socketHelper.emit({
|
||||
command: "unsubscribe",
|
||||
data: {
|
||||
roomParts,
|
||||
individual: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const newFiles: Map<string | number, TFile> = new Map();
|
||||
const newRoomParts: string[] = [];
|
||||
|
||||
files.forEach((value) => {
|
||||
const key = value.id;
|
||||
newFiles.set(key, value);
|
||||
newRoomParts.push(`FILE-${key}`);
|
||||
});
|
||||
|
||||
this.files = newFiles;
|
||||
|
||||
if (newRoomParts.length) {
|
||||
socketHelper.emit({
|
||||
command: "subscribe",
|
||||
data: {
|
||||
roomParts: newRoomParts,
|
||||
individual: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.filesStore.createThumbnails(files);
|
||||
};
|
||||
|
||||
updateFileStatus = (id: string | number | undefined, status: FileStatus) => {
|
||||
if (!id) return;
|
||||
|
||||
const item = this.files.get(id);
|
||||
|
||||
if (item) this.files.set(id, { ...item, fileStatus: status });
|
||||
};
|
||||
|
||||
getFileInfo = async (id: string | number) => {
|
||||
const fileInfo = await api.files.getFileInfo(id);
|
||||
|
||||
this.setFile(fileInfo);
|
||||
|
||||
return fileInfo;
|
||||
};
|
||||
|
||||
setFolder = (folder: TRoom | TFolder) => {
|
||||
if (!this.folders.has(folder.id)) return;
|
||||
|
||||
this.folders.set(folder.id, folder);
|
||||
};
|
||||
|
||||
setFolders = (folders: TRoom[] | TFolder[]) => {
|
||||
const { socketHelper } = this.settingsStore;
|
||||
|
||||
if (folders.length === 0) return;
|
||||
|
||||
if (this.folders.size > 0) {
|
||||
const roomParts = Array.from(this.folders.keys()).map((k) => `DIR-${k}`);
|
||||
|
||||
socketHelper.emit({
|
||||
command: "unsubscribe",
|
||||
data: {
|
||||
roomParts,
|
||||
individual: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const newFolders: Map<string | number, TRoom | TFolder> = new Map();
|
||||
const newRoomParts: string[] = [];
|
||||
|
||||
folders.forEach((value) => {
|
||||
const key = value.id;
|
||||
newFolders.set(key, value);
|
||||
newRoomParts.push(`DIR-${key}`);
|
||||
});
|
||||
|
||||
this.folders = newFolders;
|
||||
|
||||
socketHelper.emit({
|
||||
command: "subscribe",
|
||||
data: {
|
||||
roomParts: newRoomParts,
|
||||
individual: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
addFolder = (folder: TRoom | TFolder) => {
|
||||
const { socketHelper } = this.settingsStore;
|
||||
|
||||
const newFolders = new Map([
|
||||
[folder.id, folder],
|
||||
...this.folders.entries(),
|
||||
]);
|
||||
|
||||
socketHelper.emit({
|
||||
command: "subscribe",
|
||||
data: {
|
||||
roomParts: `DIR-${folder.id}`,
|
||||
individual: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.folders = newFolders;
|
||||
};
|
||||
|
||||
removeFolder = (key: string | number) => {
|
||||
this.folders.delete(key);
|
||||
};
|
||||
|
||||
updateRoomMute = (id: string | number, mute: boolean) => {
|
||||
const room = this.folders.get(id);
|
||||
|
||||
if (!room) return;
|
||||
|
||||
this.folders.set(id, { ...room, mute });
|
||||
};
|
||||
|
||||
getFolderInfo = async (id: string | number) => {
|
||||
const folderInfo = await api.files.getFolderInfo(id);
|
||||
|
||||
this.setFolder(folderInfo);
|
||||
|
||||
return folderInfo;
|
||||
};
|
||||
|
||||
clearFiles = () => {
|
||||
this.files = new Map();
|
||||
this.folders = new Map();
|
||||
|
||||
this.selectedFolderStore.setSelectedFolder(null);
|
||||
};
|
||||
|
||||
abortAllFetch = () => {
|
||||
this.filesController.abort();
|
||||
this.roomsController.abort();
|
||||
this.filesController = new AbortController();
|
||||
this.roomsController = new AbortController();
|
||||
};
|
||||
|
||||
fetchFiles = async (
|
||||
folderId: string | number,
|
||||
filter: FilesFilter,
|
||||
clearFilter: boolean = true,
|
||||
withSubfolders: boolean = false,
|
||||
clearSelection: boolean = true,
|
||||
) => {
|
||||
const { setSelectedNode } = this.treeFoldersStore;
|
||||
|
||||
if (this.clientLoadingStore.isLoading) {
|
||||
this.abortAllFetch();
|
||||
}
|
||||
|
||||
const filterData = filter ? filter.clone() : FilesFilter.getDefault();
|
||||
filterData.folder = folderId;
|
||||
|
||||
if (folderId === "@my" && this.userStore.user?.isVisitor) {
|
||||
const url = getCategoryUrl(CategoryType.Shared);
|
||||
|
||||
window.DocSpace.navigate(
|
||||
`${url}?${RoomsFilter.getDefault().toUrlParams()}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.filesStore.setIsErrorRoomNotAvailable(false);
|
||||
|
||||
const filterStorageItem =
|
||||
this.userStore.user?.id &&
|
||||
localStorage.getItem(`UserFilter=${this.userStore.user.id}`);
|
||||
|
||||
if (filterStorageItem && !filter) {
|
||||
const splitFilter = filterStorageItem.split(",");
|
||||
|
||||
filterData.sortBy = splitFilter[0] as TSortBy;
|
||||
filterData.pageCount = +splitFilter[1];
|
||||
filterData.sortOrder = splitFilter[2] as TSortOrder;
|
||||
}
|
||||
|
||||
if (!this.settingsStore.withPaging) {
|
||||
filterData.page = 0;
|
||||
filterData.pageCount = 100;
|
||||
}
|
||||
|
||||
const defaultFilter = FilesFilter.getDefault();
|
||||
|
||||
const { filterType, searchInContent } = filterData;
|
||||
|
||||
if (typeof filterData.withSubfolders !== "boolean")
|
||||
filterData.withSubfolders = defaultFilter.withSubfolders;
|
||||
|
||||
if (typeof searchInContent !== "boolean")
|
||||
filterData.searchInContent = defaultFilter.searchInContent;
|
||||
|
||||
if (!Object.keys(FilterType).find((key) => FilterType[key] === filterType))
|
||||
filterData.filterType = defaultFilter.filterType;
|
||||
|
||||
setSelectedNode([`${folderId}`]);
|
||||
|
||||
try {
|
||||
const folder = await api.files.getFolder(
|
||||
folderId,
|
||||
filterData,
|
||||
this.filesController.signal,
|
||||
);
|
||||
|
||||
let newTotal = folder.total;
|
||||
|
||||
// fixed row loader if total and items length is different
|
||||
const itemsLength = folder.folders.length + folder.files.length;
|
||||
if (itemsLength < filterData.pageCount) {
|
||||
newTotal =
|
||||
filterData.page > 0
|
||||
? itemsLength + this.files.size + this.folders.size
|
||||
: itemsLength;
|
||||
}
|
||||
|
||||
filterData.total = newTotal;
|
||||
|
||||
if (
|
||||
(folder.current.roomType === RoomsType.PublicRoom ||
|
||||
folder.current.roomType === RoomsType.FormRoom ||
|
||||
folder.current.roomType === RoomsType.CustomRoom) &&
|
||||
!this.publicRoomStore.isPublicRoom
|
||||
) {
|
||||
await this.publicRoomStore.getExternalLinks(folder.current.id);
|
||||
}
|
||||
|
||||
if (newTotal > 0) {
|
||||
const lastPage = filterData.getLastPage();
|
||||
|
||||
if (filterData.page > lastPage) {
|
||||
filterData.page = lastPage;
|
||||
|
||||
return this.fetchFiles(
|
||||
folderId,
|
||||
filterData,
|
||||
clearFilter,
|
||||
withSubfolders,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
if (!this.publicRoomStore.isPublicRoom) {
|
||||
this.filesStore.categoryType = getCategoryTypeByFolderType(
|
||||
folder.current.rootFolderType,
|
||||
folder.current.parentId,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.filesStore.isPreview) {
|
||||
// save filter for after closing preview change url
|
||||
this.filesStore.setTempFilter(filterData);
|
||||
} else {
|
||||
this.filesStore.setFilesFilter(filterData); // TODO: FILTER
|
||||
}
|
||||
|
||||
const isPrivacyFolder =
|
||||
folder.current.rootFolderType === FolderType.Privacy;
|
||||
|
||||
let inRoom = false;
|
||||
|
||||
const navigationPath = await Promise.all(
|
||||
folder.pathParts.map(async (f, idx) => {
|
||||
const { Rooms, Archive } = FolderType;
|
||||
|
||||
const isCurrentFolder = folder.current.id === f.id;
|
||||
|
||||
const folderInfo = isCurrentFolder
|
||||
? folder.current
|
||||
: { ...f, id: f.id };
|
||||
|
||||
const { title, roomType } = folderInfo;
|
||||
|
||||
inRoom = inRoom || (!!roomType && !isCurrentFolder);
|
||||
|
||||
const isRootRoom =
|
||||
idx === 0 &&
|
||||
(folder.current.rootFolderType === Rooms ||
|
||||
folder.current.rootFolderType === Archive);
|
||||
|
||||
let shared;
|
||||
let canCopyPublicLink;
|
||||
|
||||
if (idx === 1) {
|
||||
let room = folder.current;
|
||||
|
||||
if (!isCurrentFolder) {
|
||||
room = await api.files.getFolderInfo(folderId);
|
||||
shared = room.shared;
|
||||
canCopyPublicLink =
|
||||
room.access === ShareAccessRights.RoomManager ||
|
||||
room.access === ShareAccessRights.None;
|
||||
|
||||
if ("canCopyPublicLink" in room)
|
||||
room.canCopyPublicLink = canCopyPublicLink;
|
||||
this.infoPanelStore.setInfoPanelRoom(room);
|
||||
}
|
||||
|
||||
const { mute } = room;
|
||||
|
||||
runInAction(() => {
|
||||
this.filesStore.isMuteCurrentRoomNotifications = mute;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: folderId,
|
||||
title,
|
||||
isRoom: !!roomType,
|
||||
roomType,
|
||||
isRootRoom,
|
||||
shared,
|
||||
canCopyPublicLink,
|
||||
};
|
||||
}),
|
||||
).then((res) => {
|
||||
return res
|
||||
.filter((item, index) => {
|
||||
return index !== res.length - 1;
|
||||
})
|
||||
.reverse();
|
||||
});
|
||||
this.selectedFolderStore.setSelectedFolder({
|
||||
folders: folder.folders,
|
||||
...folder.current,
|
||||
inRoom,
|
||||
isRoom: !!folder.current.roomType,
|
||||
pathParts: folder.pathParts,
|
||||
navigationPath,
|
||||
...{ new: folder.new },
|
||||
// type,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
const isEmptyList = [...folder.folders, ...folder.files].length === 0;
|
||||
|
||||
if (filter && isEmptyList) {
|
||||
const {
|
||||
authorType,
|
||||
roomId,
|
||||
search,
|
||||
withSubfolders: curWithSubFolders,
|
||||
filterType: curFilterType,
|
||||
searchInContent: curSearchInContent,
|
||||
} = filter;
|
||||
|
||||
const isFiltered =
|
||||
authorType ||
|
||||
roomId ||
|
||||
search ||
|
||||
curWithSubFolders ||
|
||||
curFilterType ||
|
||||
curSearchInContent;
|
||||
|
||||
if (isFiltered) {
|
||||
this.setIsEmptyPage(false);
|
||||
} else {
|
||||
this.setIsEmptyPage(isEmptyList);
|
||||
}
|
||||
} else {
|
||||
this.setIsEmptyPage(isEmptyList);
|
||||
}
|
||||
this.setFolders(isPrivacyFolder && !isDesktop() ? [] : folder.folders);
|
||||
this.setFiles(isPrivacyFolder && !isDesktop() ? [] : folder.files);
|
||||
});
|
||||
|
||||
if (clearFilter) {
|
||||
if (clearSelection) {
|
||||
// Find not processed
|
||||
const tempSelection = this.filesStore.selection.filter(
|
||||
(f) =>
|
||||
!this.filesStore.activeFiles.find((elem) => elem.id === f.id),
|
||||
);
|
||||
const tempBuffer =
|
||||
this.filesStore.bufferSelection &&
|
||||
this.filesStore.activeFiles.find(
|
||||
(elem) => elem.id === this.filesStore.bufferSelection?.id,
|
||||
) == null
|
||||
? this.filesStore.bufferSelection
|
||||
: null;
|
||||
|
||||
// console.log({ tempSelection, tempBuffer });
|
||||
|
||||
// Clear all selections
|
||||
this.filesStore.setSelected("close");
|
||||
|
||||
// TODO: see bug 63479
|
||||
if (this.selectedFolderStore?.id === folderId) {
|
||||
// Restore not processed
|
||||
if (tempSelection.length)
|
||||
this.filesStore.setSelection(tempSelection);
|
||||
if (tempBuffer) this.filesStore.setBufferSelection(tempBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.clientLoadingStore.setIsSectionHeaderLoading(false);
|
||||
|
||||
const selectedFolder = {
|
||||
selectedFolder: { ...this.selectedFolderStore },
|
||||
};
|
||||
|
||||
if (this.filesStore.createdItem) {
|
||||
const newItem = this.filesList.find(
|
||||
(item) => item.id === this.filesStore.createdItem?.id,
|
||||
);
|
||||
|
||||
if (newItem) {
|
||||
this.filesStore.setBufferSelection(newItem);
|
||||
this.filesStore.setScrollToItem({
|
||||
id: newItem.id,
|
||||
type: this.filesStore.createdItem?.type,
|
||||
});
|
||||
}
|
||||
|
||||
this.filesStore.setCreatedItem(null);
|
||||
}
|
||||
|
||||
if (isPublicRoom()) {
|
||||
return folder;
|
||||
}
|
||||
|
||||
return selectedFolder;
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 402)
|
||||
this.filesStore.currentTariffStatusStore.setPortalTariff();
|
||||
|
||||
const isThirdPartyError = Number.isNaN(+folderId);
|
||||
|
||||
if (requestCounter > 0 && !isThirdPartyError) return;
|
||||
|
||||
requestCounter = +1;
|
||||
|
||||
const isUserError = [
|
||||
NotFoundHttpCode,
|
||||
ForbiddenHttpCode,
|
||||
PaymentRequiredHttpCode,
|
||||
UnauthorizedHttpCode,
|
||||
].includes(err?.response?.status);
|
||||
|
||||
if (isUserError && !isThirdPartyError) {
|
||||
this.filesStore.setIsErrorRoomNotAvailable(true);
|
||||
} else if (axios.isCancel(err)) {
|
||||
console.log("Request canceled", err.message);
|
||||
} else {
|
||||
toastr.error(err);
|
||||
if (isThirdPartyError) {
|
||||
const userId = this.userStore?.user?.id;
|
||||
const searchArea = window.DocSpace.location.pathname.includes(
|
||||
"shared",
|
||||
)
|
||||
? RoomSearchArea.Active
|
||||
: RoomSearchArea.Archive;
|
||||
|
||||
window.DocSpace.navigate(
|
||||
`${window.DocSpace.location.pathname}?${RoomsFilter.getDefault(userId, searchArea).toUrlParams(userId, true)}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (window?.DocSpace?.location?.state?.highlightFileId) {
|
||||
this.filesStore.setHighlightFile({
|
||||
highlightFileId: window.DocSpace.location.state.highlightFileId,
|
||||
isFileHasExst: window.DocSpace.location.state.isFileHasExst,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchRooms = async (
|
||||
folderId,
|
||||
filter,
|
||||
clearFilter = true,
|
||||
withSubfolders = false,
|
||||
clearSelection = true,
|
||||
withFilterLocalStorage = false,
|
||||
) => {
|
||||
const { setSelectedNode } = this.treeFoldersStore;
|
||||
|
||||
if (this.clientLoadingStore.isLoading) {
|
||||
this.abortAllFetch();
|
||||
}
|
||||
|
||||
const filterData = filter
|
||||
? filter.clone()
|
||||
: RoomsFilter.getDefault(this.userStore.user?.id);
|
||||
|
||||
if (!this.settingsStore.withPaging) {
|
||||
const isCustomCountPage =
|
||||
filter && filter.pageCount !== 100 && filter.pageCount !== 25;
|
||||
|
||||
if (!isCustomCountPage) {
|
||||
filterData.page = 0;
|
||||
filterData.pageCount = 100;
|
||||
}
|
||||
}
|
||||
|
||||
if (folderId) setSelectedNode([`${folderId}`]);
|
||||
|
||||
const defaultFilter = RoomsFilter.getDefault();
|
||||
|
||||
const { provider, quotaFilter, type } = filterData;
|
||||
|
||||
if (!ROOMS_PROVIDER_TYPE_NAME[provider])
|
||||
filterData.provider = defaultFilter.provider;
|
||||
|
||||
if (
|
||||
quotaFilter &&
|
||||
quotaFilter !== FilterKeys.customQuota &&
|
||||
quotaFilter !== FilterKeys.defaultQuota
|
||||
)
|
||||
filterData.quotaFilter = defaultFilter.quotaFilter;
|
||||
|
||||
if (type && !RoomsType[type]) filterData.type = defaultFilter.type;
|
||||
|
||||
try {
|
||||
const rooms = await api.rooms.getRooms(
|
||||
filterData,
|
||||
this.roomsController.signal,
|
||||
);
|
||||
|
||||
if (!folderId) setSelectedNode([`${rooms.current.id}`]);
|
||||
|
||||
filterData.total = rooms.total;
|
||||
|
||||
if (rooms.total > 0) {
|
||||
const lastPage = filterData.getLastPage();
|
||||
|
||||
if (filterData.page > lastPage) {
|
||||
filterData.page = lastPage;
|
||||
|
||||
return this.fetchRooms(
|
||||
folderId,
|
||||
filterData,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.filesStore.categoryType = getCategoryTypeByFolderType(
|
||||
rooms.current.rootFolderType,
|
||||
rooms.current.parentId,
|
||||
);
|
||||
});
|
||||
|
||||
this.filesStore.setRoomsFilter(filterData);
|
||||
|
||||
runInAction(() => {
|
||||
const isEmptyList = rooms.folders.length === 0;
|
||||
if (filter && isEmptyList) {
|
||||
const {
|
||||
subjectId,
|
||||
filterValue,
|
||||
type: curType,
|
||||
withSubfolders: withRoomsSubfolders,
|
||||
searchInContent: searchInContentRooms,
|
||||
tags,
|
||||
withoutTags,
|
||||
quotaFilter: curQuotaFilter,
|
||||
provider: curProvider,
|
||||
} = filter;
|
||||
|
||||
const isFiltered =
|
||||
subjectId ||
|
||||
filterValue ||
|
||||
curType ||
|
||||
curProvider ||
|
||||
withRoomsSubfolders ||
|
||||
searchInContentRooms ||
|
||||
tags ||
|
||||
withoutTags ||
|
||||
curQuotaFilter;
|
||||
|
||||
if (isFiltered) {
|
||||
this.setIsEmptyPage(false);
|
||||
} else {
|
||||
this.setIsEmptyPage(isEmptyList);
|
||||
}
|
||||
} else {
|
||||
this.setIsEmptyPage(isEmptyList);
|
||||
}
|
||||
|
||||
this.setFolders(rooms.folders);
|
||||
this.setFiles([]);
|
||||
|
||||
if (clearFilter) {
|
||||
if (clearSelection) {
|
||||
this.filesStore.setSelected("close");
|
||||
}
|
||||
}
|
||||
|
||||
this.infoPanelStore.setInfoPanelRoom(null);
|
||||
this.selectedFolderStore.setSelectedFolder({
|
||||
folders: rooms.folders,
|
||||
...rooms.current,
|
||||
pathParts: rooms.pathParts,
|
||||
navigationPath: [],
|
||||
...{ new: rooms.new },
|
||||
});
|
||||
|
||||
this.clientLoadingStore.setIsSectionHeaderLoading(false);
|
||||
|
||||
const selectedFolder = {
|
||||
selectedFolder: { ...this.selectedFolderStore },
|
||||
};
|
||||
|
||||
if (this.filesStore.createdItem) {
|
||||
const newItem = this.filesStore.filesList.find(
|
||||
(item) => item.id === this.filesStore.createdItem?.id,
|
||||
);
|
||||
|
||||
if (newItem) {
|
||||
this.filesStore.setBufferSelection(newItem);
|
||||
this.filesStore.setScrollToItem({
|
||||
id: newItem.id,
|
||||
type: this.filesStore.createdItem?.type,
|
||||
});
|
||||
}
|
||||
|
||||
this.filesStore.setCreatedItem(null);
|
||||
}
|
||||
this.filesStore.setIsErrorRoomNotAvailable(false);
|
||||
|
||||
return selectedFolder;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 402)
|
||||
this.filesStore.currentTariffStatusStore.setPortalTariff();
|
||||
|
||||
if (axios.isCancel(err)) {
|
||||
console.log("Request canceled", err.message);
|
||||
} else {
|
||||
toastr.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getFilesListItems = (items: (TFile | TFolder | TRoom)[]) => {
|
||||
const { fileItemsList } = this.pluginStore;
|
||||
const { enablePlugins } = this.settingsStore;
|
||||
const { getIcon } = this.filesSettingsStore;
|
||||
|
||||
return items.map((item) => {
|
||||
const { id, rootFolderId, access } = item;
|
||||
|
||||
let thirdPartyIcon = "";
|
||||
let providerType = "";
|
||||
|
||||
if ("providerKey" in item) {
|
||||
thirdPartyIcon = this.thirdPartyStore.getThirdPartyIcon(
|
||||
item.providerKey,
|
||||
"small",
|
||||
);
|
||||
}
|
||||
|
||||
if ("providerKey" in item) {
|
||||
providerType =
|
||||
RoomsProviderType[
|
||||
Object.keys(RoomsProviderType).find(
|
||||
(key) => key === item.providerKey,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
let canOpenPlayer = false;
|
||||
let needConvert = false;
|
||||
|
||||
if ("viewAccessibility" in item) {
|
||||
canOpenPlayer =
|
||||
item.viewAccessibility?.ImageView ||
|
||||
item.viewAccessibility?.MediaView;
|
||||
|
||||
needConvert = item.viewAccessibility?.MustConvert;
|
||||
}
|
||||
|
||||
const previewUrl = canOpenPlayer
|
||||
? this.filesStore.getItemUrl(id, false, needConvert, canOpenPlayer)
|
||||
: null;
|
||||
|
||||
const contextOptions = this.filesStore.getFilesContextOptions(item);
|
||||
const isThirdPartyFolder =
|
||||
"providerKey" in item && item.providerKey && id === rootFolderId;
|
||||
|
||||
const iconSize = this.filesStore.viewAs === "table" ? 24 : 32;
|
||||
|
||||
let isFolder = false;
|
||||
|
||||
if ("parentId" in item) {
|
||||
this.folders.forEach((value) => {
|
||||
if (value.id === item.id && value.parentId === item.parentId)
|
||||
isFolder = true;
|
||||
});
|
||||
}
|
||||
|
||||
const { isRecycleBinFolder } = this.treeFoldersStore;
|
||||
|
||||
const folderUrl =
|
||||
isFolder && this.filesStore.getItemUrl(id, isFolder, false, false);
|
||||
|
||||
const isEditing =
|
||||
"fileStatus" in item && item.fileStatus === FileStatus.IsEditing;
|
||||
|
||||
const docUrl =
|
||||
!canOpenPlayer &&
|
||||
!isFolder &&
|
||||
this.filesStore.getItemUrl(id, false, needConvert);
|
||||
|
||||
const href = isRecycleBinFolder
|
||||
? null
|
||||
: previewUrl || (!isFolder ? docUrl : folderUrl);
|
||||
|
||||
const isRoom = "roomType" in item && !!item.roomType;
|
||||
|
||||
const logo = "logo" in item ? item.logo : null;
|
||||
|
||||
const fileExst = "fileExst" in item ? item.fileExst : undefined;
|
||||
const providerKey = "providerKey" in item ? item.providerKey : null;
|
||||
const contentLength =
|
||||
"contentLength" in item ? item.contentLength : undefined;
|
||||
const roomType = "roomType" in item ? item.roomType : undefined;
|
||||
const isArchive = "isArchive" in item ? item.isArchive : undefined;
|
||||
const type = "type" in item ? item.type : undefined;
|
||||
|
||||
const icon =
|
||||
isRoom && logo?.medium
|
||||
? logo?.medium
|
||||
: getIcon(
|
||||
iconSize,
|
||||
fileExst,
|
||||
providerKey,
|
||||
contentLength,
|
||||
roomType,
|
||||
isArchive,
|
||||
type,
|
||||
);
|
||||
|
||||
const defaultRoomIcon = isRoom
|
||||
? getIcon(
|
||||
iconSize,
|
||||
fileExst,
|
||||
providerKey,
|
||||
contentLength,
|
||||
roomType,
|
||||
isArchive,
|
||||
type,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const pluginOptions = {
|
||||
fileTypeName: "",
|
||||
isPlugin: false,
|
||||
fileTileIcon: "",
|
||||
};
|
||||
|
||||
if (enablePlugins && fileItemsList) {
|
||||
fileItemsList.forEach(({ value }) => {
|
||||
if (value.extension === fileExst) {
|
||||
if (value.fileTypeName)
|
||||
pluginOptions.fileTypeName = value.fileTypeName;
|
||||
pluginOptions.isPlugin = true;
|
||||
if (value.fileIconTile)
|
||||
pluginOptions.fileTileIcon = value.fileIconTile;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isForm = fileExst === ".oform";
|
||||
|
||||
const canCopyPublicLink =
|
||||
access === ShareAccessRights.RoomManager ||
|
||||
access === ShareAccessRights.None;
|
||||
|
||||
return {
|
||||
...item,
|
||||
|
||||
access,
|
||||
daysRemaining:
|
||||
"autoDelete" in item &&
|
||||
item.autoDelete &&
|
||||
getDaysRemaining(item.autoDelete),
|
||||
|
||||
contentLength,
|
||||
contextOptions,
|
||||
|
||||
fileExst,
|
||||
|
||||
icon,
|
||||
defaultRoomIcon,
|
||||
id,
|
||||
isFolder,
|
||||
logo,
|
||||
|
||||
rootFolderId,
|
||||
|
||||
providerKey,
|
||||
canOpenPlayer,
|
||||
|
||||
previewUrl,
|
||||
folderUrl,
|
||||
href,
|
||||
isThirdPartyFolder,
|
||||
isEditing,
|
||||
roomType,
|
||||
isRoom,
|
||||
isArchive,
|
||||
|
||||
thirdPartyIcon,
|
||||
providerType,
|
||||
|
||||
...pluginOptions,
|
||||
|
||||
type,
|
||||
|
||||
isForm,
|
||||
canCopyPublicLink,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
get filesList() {
|
||||
const newFolders = Array.from(this.folders.values());
|
||||
|
||||
newFolders.sort((a, b) => {
|
||||
const firstValue = a.roomType ? 1 : 0;
|
||||
const secondValue = b.roomType ? 1 : 0;
|
||||
|
||||
return secondValue - firstValue;
|
||||
});
|
||||
|
||||
const items = [...newFolders, ...Array.from(this.files.values())];
|
||||
|
||||
if (items.length > 0 && this.isEmptyPage) {
|
||||
this.setIsEmptyPage(false);
|
||||
}
|
||||
|
||||
return this.getFilesListItems(items);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilesListStore;
|
35
packages/client/src/store/FilesSelectionStore.ts
Normal file
35
packages/client/src/store/FilesSelectionStore.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
import { TFile, TFolder } from "@docspace/shared/api/files/types";
|
||||
import { TRoom } from "@docspace/shared/api/rooms/types";
|
||||
import { Nullable } from "@docspace/shared/types";
|
||||
|
||||
export type TSelection = (TFile | TFolder | TRoom)[];
|
||||
export type TBufferSelection = Nullable<TFile | TFolder | TRoom>;
|
||||
export type TSelected = "close" | "none";
|
||||
|
||||
class FilesSelectionStore {
|
||||
selection: TSelection = [];
|
||||
|
||||
bufferSelection: TBufferSelection = null;
|
||||
|
||||
selected: TSelected = "close";
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setSelection = (selection: TSelection) => {
|
||||
this.selection = selection;
|
||||
};
|
||||
|
||||
setBufferSelection = (bufferSelection: TBufferSelection) => {
|
||||
this.bufferSelection = bufferSelection;
|
||||
};
|
||||
|
||||
setSelected = (selected: TSelected) => {
|
||||
this.selected = selected;
|
||||
};
|
||||
}
|
||||
|
||||
export default FilesSelectionStore;
|
564
packages/client/src/store/FilesSocketStore.ts
Normal file
564
packages/client/src/store/FilesSocketStore.ts
Normal file
@ -0,0 +1,564 @@
|
||||
/* eslint-disable no-console */
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import merge from "lodash/merge";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
import api from "@docspace/shared/api";
|
||||
import { TFile } from "@docspace/shared/api/files/types";
|
||||
import { TOptSocket } from "@docspace/shared/utils/socket";
|
||||
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
|
||||
import { UserStore } from "@docspace/shared/store/UserStore";
|
||||
import { Events, FileStatus } from "@docspace/shared/enums";
|
||||
import { PDF_FORM_DIALOG_KEY } from "@docspace/shared/constants";
|
||||
|
||||
import FilesStore from "./FilesStore";
|
||||
import ClientLoadingStore from "./ClientLoadingStore";
|
||||
import SelectedFolderStore from "./SelectedFolderStore";
|
||||
import TreeFoldersStore from "./TreeFoldersStore";
|
||||
import InfoPanelStore from "./InfoPanelStore";
|
||||
import FilesListStore from "./FilesListStore";
|
||||
|
||||
class FilesSocketStore {
|
||||
constructor(
|
||||
private settingsStore: Readonly<SettingsStore>,
|
||||
private clientLoadingStore: Readonly<ClientLoadingStore>,
|
||||
private selectedFolderStore: Readonly<SelectedFolderStore>,
|
||||
private treeFoldersStore: Readonly<TreeFoldersStore>,
|
||||
private infoPanelStore: Readonly<InfoPanelStore>,
|
||||
private userStore: Readonly<UserStore>,
|
||||
|
||||
private filesListStore: Readonly<FilesListStore>,
|
||||
|
||||
private filesStore: Readonly<FilesStore>,
|
||||
) {
|
||||
makeAutoObservable(this);
|
||||
|
||||
const { socketHelper } = settingsStore;
|
||||
|
||||
socketHelper.on("s:modify-folder", async (opt) => {
|
||||
const { socketSubscribers } = socketHelper;
|
||||
|
||||
if (opt && typeof opt !== "string" && opt.data) {
|
||||
const data = JSON.parse(opt.data);
|
||||
|
||||
const pathParts = data.folderId
|
||||
? `DIR-${data.folderId}`
|
||||
: `DIR-${data.parentId}`;
|
||||
|
||||
if (
|
||||
!socketSubscribers.has(pathParts) &&
|
||||
!socketSubscribers.has(`DIR-${data.id}`)
|
||||
) {
|
||||
console.log("[WS] s:modify-folder: SKIP UNSUBSCRIBED", { data });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[WS] s:modify-folder", opt);
|
||||
|
||||
if (
|
||||
!(this.clientLoadingStore.isLoading || this.filesStore.operationAction)
|
||||
) {
|
||||
switch (typeof opt !== "string" && opt?.cmd) {
|
||||
case "create":
|
||||
this.wsModifyFolderCreate(opt);
|
||||
break;
|
||||
case "update":
|
||||
this.wsModifyFolderUpdate(opt);
|
||||
break;
|
||||
case "delete":
|
||||
this.wsModifyFolderDelete(opt);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof opt !== "string" &&
|
||||
opt?.cmd &&
|
||||
opt.id &&
|
||||
(opt.type === "file" || opt.type === "folder") &&
|
||||
(opt.cmd === "create" || opt.cmd === "delete")
|
||||
) {
|
||||
if (opt.type === "file") {
|
||||
if (opt.cmd === "create") {
|
||||
this.selectedFolderStore.increaseFilesCount();
|
||||
} else {
|
||||
this.selectedFolderStore.decreaseFilesCount();
|
||||
}
|
||||
} else if (opt.type === "folder")
|
||||
if (opt.cmd === "create") {
|
||||
this.selectedFolderStore.increaseFoldersCount();
|
||||
} else {
|
||||
this.selectedFolderStore.decreaseFoldersCount();
|
||||
}
|
||||
}
|
||||
|
||||
this.treeFoldersStore.updateTreeFoldersItem(opt);
|
||||
});
|
||||
|
||||
socketHelper.on("s:update-history", (opt) => {
|
||||
if (typeof opt === "string") return;
|
||||
|
||||
const { infoPanelSelection, fetchHistory } = this.infoPanelStore;
|
||||
|
||||
const { id, type } = opt;
|
||||
|
||||
let infoPanelSelectionType = "file";
|
||||
if (infoPanelSelection?.isRoom || infoPanelSelection?.isFolder)
|
||||
infoPanelSelectionType = "folder";
|
||||
|
||||
if (id === infoPanelSelection?.id && type === infoPanelSelectionType) {
|
||||
console.log("[WS] s:update-history", id);
|
||||
fetchHistory();
|
||||
}
|
||||
});
|
||||
|
||||
socketHelper.on("refresh-folder", (id) => {
|
||||
const { socketSubscribers } = socketHelper;
|
||||
const pathParts = `DIR-${id}`;
|
||||
|
||||
if (!socketSubscribers.has(pathParts)) return;
|
||||
|
||||
if (!id || this.clientLoadingStore.isLoading) return;
|
||||
|
||||
if (
|
||||
this.selectedFolderStore.id?.toString() === id.toString() &&
|
||||
this.settingsStore.withPaging // TODO: no longer deletes the folder in other tabs
|
||||
) {
|
||||
console.log("[WS] refresh-folder", id);
|
||||
this.filesStore.fetchFiles(id, this.filesStore.filter);
|
||||
}
|
||||
});
|
||||
|
||||
socketHelper.on("s:markasnew-folder", (opt) => {
|
||||
if (typeof opt === "string") return;
|
||||
|
||||
const { socketSubscribers } = socketHelper;
|
||||
|
||||
const { folderId, count } = opt;
|
||||
const pathParts = `DIR-${folderId}`;
|
||||
|
||||
if (!socketSubscribers.has(pathParts)) return;
|
||||
|
||||
console.log(`[WS] markasnew-folder ${folderId}:${count}`);
|
||||
|
||||
const foundIndex =
|
||||
folderId && this.filesStore.folders.findIndex((x) => x.id === folderId);
|
||||
if (foundIndex === -1 || !foundIndex) return;
|
||||
|
||||
runInAction(() => {
|
||||
this.filesStore.folders[foundIndex].new =
|
||||
typeof count !== "undefined" && Number(count) >= 0 ? count : 0;
|
||||
this.treeFoldersStore.fetchTreeFolders();
|
||||
});
|
||||
});
|
||||
|
||||
socketHelper.on("s:markasnew-file", (opt) => {
|
||||
if (typeof opt === "string") return;
|
||||
const { fileId, count } = opt;
|
||||
|
||||
if (!fileId) return;
|
||||
|
||||
const { socketSubscribers } = socketHelper;
|
||||
const pathParts = `FILE-${fileId}`;
|
||||
|
||||
if (!socketSubscribers.has(pathParts)) return;
|
||||
|
||||
console.log(`[WS] markasnew-file ${fileId}:${count}`);
|
||||
|
||||
this.treeFoldersStore.fetchTreeFolders();
|
||||
|
||||
const fileStatus = this.filesListStore.files.get(fileId)?.fileStatus;
|
||||
|
||||
const status =
|
||||
typeof count !== "undefined" && Number(count) > 0 && !fileStatus
|
||||
? FileStatus.IsNew
|
||||
: fileStatus === FileStatus.IsNew
|
||||
? FileStatus.None
|
||||
: fileStatus || FileStatus.None;
|
||||
|
||||
if (status !== fileStatus)
|
||||
this.filesListStore.updateFileStatus(fileId, status);
|
||||
});
|
||||
|
||||
// WAIT FOR RESPONSES OF EDITING FILE
|
||||
socketHelper.on("s:start-edit-file", (id) => {
|
||||
if (typeof id !== "string") return;
|
||||
|
||||
const { socketSubscribers } = socketHelper;
|
||||
|
||||
const pathParts = `FILE-${id}`;
|
||||
|
||||
if (!socketSubscribers.has(pathParts)) return;
|
||||
|
||||
console.log(`[WS] s:start-edit-file`, id);
|
||||
|
||||
const fileStatus = this.filesListStore.files.get(id)?.fileStatus;
|
||||
|
||||
this.filesStore.updateSelectionStatus(
|
||||
id,
|
||||
fileStatus || FileStatus.IsEditing,
|
||||
true,
|
||||
);
|
||||
|
||||
this.filesListStore.updateFileStatus(
|
||||
id,
|
||||
fileStatus || FileStatus.IsEditing,
|
||||
);
|
||||
});
|
||||
|
||||
socketHelper.on("s:modify-room", (option) => {
|
||||
if (typeof option === "string") return;
|
||||
switch (option.cmd) {
|
||||
case "create-form":
|
||||
this.wsCreatedPDFForm(option);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
socketHelper.on("s:stop-edit-file", (id) => {
|
||||
if (typeof id !== "string") return;
|
||||
|
||||
const { socketSubscribers } = socketHelper;
|
||||
const pathParts = `FILE-${id}`;
|
||||
|
||||
const { isVisible, infoPanelSelection, setInfoPanelSelection } =
|
||||
this.infoPanelStore;
|
||||
|
||||
if (!socketSubscribers.has(pathParts)) return;
|
||||
|
||||
console.log(`[WS] s:stop-edit-file`, id);
|
||||
|
||||
const currFile = this.filesListStore.files.get(id);
|
||||
const fileStatus = currFile?.fileStatus;
|
||||
const status =
|
||||
fileStatus === FileStatus.IsEditing
|
||||
? FileStatus.None
|
||||
: fileStatus || FileStatus.None;
|
||||
|
||||
this.filesStore.updateSelectionStatus(id, status, false);
|
||||
|
||||
this.filesListStore.updateFileStatus(id, status);
|
||||
|
||||
this.filesStore.getFileInfo(id).then((file) => {
|
||||
if (
|
||||
isVisible &&
|
||||
file.id === infoPanelSelection?.id &&
|
||||
infoPanelSelection?.fileExst === file.fileExst
|
||||
) {
|
||||
setInfoPanelSelection(merge(cloneDeep(infoPanelSelection), file));
|
||||
}
|
||||
});
|
||||
|
||||
this.filesStore.createThumbnail(currFile);
|
||||
});
|
||||
|
||||
this.filesStore.createNewFilesQueue.on("resolve", this.onResolveNewFile);
|
||||
}
|
||||
|
||||
wsModifyFolderCreate = async (opt: TOptSocket | string) => {
|
||||
if (typeof opt === "string") return;
|
||||
|
||||
if (opt?.type === "file" && opt?.id && opt.data) {
|
||||
const curFile = this.filesListStore.files.get(opt.id);
|
||||
|
||||
const file = JSON.parse(opt?.data);
|
||||
|
||||
if (this.selectedFolderStore.id !== file.folderId) {
|
||||
const folder = this.filesListStore.folders.get(file.folderId);
|
||||
if (folder)
|
||||
this.filesListStore.folders.set(folder.id, {
|
||||
...folder,
|
||||
filesCount: folder.filesCount + 1,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// To update a file version
|
||||
if (curFile && !this.settingsStore.withPaging) {
|
||||
if (
|
||||
curFile.version !== file.version ||
|
||||
curFile.versionGroup !== file.versionGroup
|
||||
) {
|
||||
curFile.version = file.version;
|
||||
curFile.versionGroup = file.versionGroup;
|
||||
}
|
||||
this.filesStore.checkSelection(file);
|
||||
}
|
||||
|
||||
if (curFile) return;
|
||||
|
||||
setTimeout(() => {
|
||||
const foundFile = this.filesListStore.files.get(file.id);
|
||||
|
||||
if (foundFile) {
|
||||
// console.log("Skip in timeout");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.filesStore.createNewFilesQueue.enqueue(() => {
|
||||
const foundedFile = this.filesListStore.files.get(file.id);
|
||||
|
||||
if (foundedFile) {
|
||||
// console.log("Skip in queue");
|
||||
return null;
|
||||
}
|
||||
|
||||
return api.files.getFileInfo(file.id);
|
||||
});
|
||||
}, 300);
|
||||
} else if (opt?.type === "folder" && opt?.id && opt?.data) {
|
||||
const curFolder = this.filesListStore.folders.get(opt.id);
|
||||
|
||||
if (curFolder) return;
|
||||
|
||||
const folder = JSON.parse(opt?.data);
|
||||
|
||||
if (
|
||||
this.selectedFolderStore.id?.toString() !== folder.parentId.toString()
|
||||
) {
|
||||
const parentFolder = this.filesListStore.folders.get(folder?.parentId);
|
||||
|
||||
if (parentFolder)
|
||||
this.filesListStore.folders.set(parentFolder.id, {
|
||||
...parentFolder,
|
||||
filesCount: folder.filesCount + 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.selectedFolderStore.id !== folder.parentId ||
|
||||
(folder.roomType &&
|
||||
folder.createdBy.id === this.userStore?.user?.id &&
|
||||
this.filesStore.roomCreated)
|
||||
) {
|
||||
return this.filesStore.setRoomCreated(false);
|
||||
}
|
||||
|
||||
const folderInfo = await api.files.getFolderInfo(folder.id);
|
||||
|
||||
console.log("[WS] create new folder", folderInfo.id, folderInfo.title);
|
||||
|
||||
const newFolders = new Map([
|
||||
[folder.id, folderInfo],
|
||||
...this.filesListStore.folders.entries(),
|
||||
]);
|
||||
|
||||
if (
|
||||
this.filesListStore.folders.size > this.filesStore.filter.pageCount &&
|
||||
this.settingsStore.withPaging
|
||||
) {
|
||||
this.filesListStore.removeFolder(Array.from(newFolders.keys()).pop()); // Remove last
|
||||
}
|
||||
|
||||
const newFilter = this.filesStore.filter;
|
||||
newFilter.total += 1;
|
||||
|
||||
runInAction(() => {
|
||||
this.filesStore.setFilter(newFilter);
|
||||
this.filesListStore.addFolder(folderInfo);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
wsModifyFolderUpdate = (opt: TOptSocket | string) => {
|
||||
if (typeof opt === "string") return;
|
||||
|
||||
if (opt?.type === "file" && opt?.data) {
|
||||
const file = JSON.parse(opt?.data);
|
||||
if (!file || !file.id) return;
|
||||
|
||||
this.filesStore.getFileInfo(file.id); // this.setFile(file);
|
||||
console.log("[WS] update file", file.id, file.title);
|
||||
|
||||
this.filesStore.checkSelection(file);
|
||||
} else if (opt?.type === "folder" && opt?.data) {
|
||||
const folder = JSON.parse(opt?.data);
|
||||
if (!folder || !folder.id) return;
|
||||
|
||||
api.files
|
||||
.getFolderInfo(folder.id)
|
||||
.then(this.filesListStore.setFolder)
|
||||
.catch(() => {
|
||||
// console.log("Folder deleted")
|
||||
});
|
||||
|
||||
console.log("[WS] update folder", folder.id, folder.title);
|
||||
|
||||
if (this.filesStore.selection?.length) {
|
||||
const foundIndex = this.filesStore.selection?.findIndex(
|
||||
(x) => x.id === folder.id,
|
||||
);
|
||||
if (foundIndex > -1) {
|
||||
runInAction(() => {
|
||||
this.filesStore.selection[foundIndex] = folder;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.filesStore.bufferSelection) {
|
||||
const foundIndex = [this.filesStore.bufferSelection].findIndex(
|
||||
(x) => x.id === folder.id,
|
||||
);
|
||||
if (foundIndex > -1) {
|
||||
runInAction(() => {
|
||||
this.filesStore.bufferSelection[foundIndex] = folder;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (folder.id === this.selectedFolderStore.id) {
|
||||
this.selectedFolderStore.setSelectedFolder({ ...folder });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
wsModifyFolderDelete = (opt: TOptSocket | string) => {
|
||||
if (typeof opt === "string") return;
|
||||
|
||||
if (opt?.type === "file" && opt?.id) {
|
||||
const foundIndex = this.filesStore.files.findIndex(
|
||||
(x) => x.id === opt?.id,
|
||||
);
|
||||
if (foundIndex === -1) return;
|
||||
|
||||
console.log(
|
||||
"[WS] delete file",
|
||||
this.filesStore.files[foundIndex].id,
|
||||
this.filesStore.files[foundIndex].title,
|
||||
);
|
||||
|
||||
const tempActionFilesIds = JSON.parse(
|
||||
JSON.stringify(this.filesStore.tempActionFilesIds),
|
||||
);
|
||||
tempActionFilesIds.push(this.filesStore.files[foundIndex].id);
|
||||
|
||||
this.filesStore.setTempActionFilesIds(tempActionFilesIds);
|
||||
|
||||
this.debounceRemoveFiles();
|
||||
|
||||
// Hide pagination when deleting files
|
||||
runInAction(() => {
|
||||
this.filesStore.isHidePagination = true;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (
|
||||
this.filesStore.files.length === 0 &&
|
||||
this.filesStore.folders.length === 0 &&
|
||||
this.filesStore.pageItemsLength > 1
|
||||
) {
|
||||
this.filesStore.isLoadingFilesFind = true;
|
||||
}
|
||||
});
|
||||
} else if (opt?.type === "folder" && opt?.id) {
|
||||
const foundIndex = this.filesStore.folders.findIndex(
|
||||
(x) => x.id === opt?.id,
|
||||
);
|
||||
if (foundIndex == -1) return;
|
||||
|
||||
console.log(
|
||||
"[WS] delete folder",
|
||||
this.filesStore.folders[foundIndex].id,
|
||||
this.filesStore.folders[foundIndex].title,
|
||||
);
|
||||
|
||||
const tempActionFoldersIds = JSON.parse(
|
||||
JSON.stringify(this.filesStore.tempActionFoldersIds),
|
||||
);
|
||||
tempActionFoldersIds.push(this.filesStore.folders[foundIndex].id);
|
||||
|
||||
this.filesStore.setTempActionFoldersIds(tempActionFoldersIds);
|
||||
this.debounceRemoveFolders();
|
||||
|
||||
runInAction(() => {
|
||||
this.filesStore.isHidePagination = true;
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (
|
||||
this.filesStore.files.length === 0 &&
|
||||
this.filesStore.folders.length === 0 &&
|
||||
this.filesStore.pageItemsLength > 1
|
||||
) {
|
||||
this.filesStore.isLoadingFilesFind = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
wsCreatedPDFForm = (option: TOptSocket) => {
|
||||
if (!option.data) return;
|
||||
|
||||
const file = JSON.parse(option.data);
|
||||
|
||||
if (this.selectedFolderStore.id !== file.folderId) return;
|
||||
|
||||
const localKey = `${PDF_FORM_DIALOG_KEY}-${this.userStore?.user?.id}`;
|
||||
|
||||
const isFirst = JSON.parse(localStorage.getItem(localKey) ?? "true");
|
||||
|
||||
const event = new CustomEvent(Events.CREATE_PDF_FORM_FILE, {
|
||||
detail: {
|
||||
file,
|
||||
isFill: !option.isOneMember,
|
||||
isFirst,
|
||||
},
|
||||
});
|
||||
|
||||
if (isFirst) localStorage.setItem(localKey, "false");
|
||||
|
||||
window?.dispatchEvent(event);
|
||||
};
|
||||
|
||||
onResolveNewFile = (fileInfo: TFile) => {
|
||||
if (!fileInfo) return;
|
||||
|
||||
if (this.filesStore.files.findIndex((x) => x.id === fileInfo.id) > -1)
|
||||
return;
|
||||
|
||||
if (this.selectedFolderStore.id !== fileInfo.folderId) return;
|
||||
|
||||
console.log("[WS] create new file", { fileInfo });
|
||||
|
||||
const newFiles = [fileInfo, ...this.filesStore.files];
|
||||
|
||||
if (
|
||||
newFiles.length > this.filesStore.filter.pageCount &&
|
||||
this.settingsStore.withPaging
|
||||
) {
|
||||
newFiles.pop(); // Remove last
|
||||
}
|
||||
|
||||
const newFilter = this.filesStore.filter;
|
||||
newFilter.total += 1;
|
||||
|
||||
runInAction(() => {
|
||||
this.filesStore.setFilter(newFilter);
|
||||
this.filesStore.setFiles(newFiles);
|
||||
});
|
||||
|
||||
this.debounceFetchTreeFolders();
|
||||
};
|
||||
|
||||
debounceFetchTreeFolders = debounce(() => {
|
||||
this.treeFoldersStore.fetchTreeFolders();
|
||||
}, 1000);
|
||||
|
||||
debounceRemoveFiles = debounce(() => {
|
||||
this.filesStore.removeFiles(this.filesStore.tempActionFilesIds);
|
||||
}, 1000);
|
||||
|
||||
debounceRemoveFolders = debounce(() => {
|
||||
this.filesStore.removeFiles(null, this.filesStore.tempActionFoldersIds);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export default FilesSocketStore;
|
File diff suppressed because it is too large
Load Diff
740
packages/client/src/store/OAuthStore.ts
Normal file
740
packages/client/src/store/OAuthStore.ts
Normal file
@ -0,0 +1,740 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
import {
|
||||
addClient,
|
||||
updateClient,
|
||||
changeClientStatus,
|
||||
regenerateSecret,
|
||||
deleteClient,
|
||||
getClientList,
|
||||
getScopeList,
|
||||
getConsentList,
|
||||
revokeUserClient,
|
||||
} from "@docspace/shared/api/oauth";
|
||||
import {
|
||||
IClientListProps,
|
||||
IClientProps,
|
||||
IClientReqDTO,
|
||||
TScope,
|
||||
} from "@docspace/shared/utils/oauth/types";
|
||||
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 { Nullable, TTranslation } from "@docspace/shared/types";
|
||||
|
||||
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 SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
|
||||
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
|
||||
|
||||
const PAGE_LIMIT = 100;
|
||||
|
||||
export type ViewAsType = "table" | "row";
|
||||
|
||||
export interface OAuthStoreProps {
|
||||
isInit: boolean;
|
||||
setIsInit: (value: boolean) => void;
|
||||
|
||||
viewAs: ViewAsType;
|
||||
setViewAs: (value: ViewAsType) => void;
|
||||
|
||||
infoDialogVisible: boolean;
|
||||
setInfoDialogVisible: (value: boolean) => void;
|
||||
|
||||
revokeDialogVisible: boolean;
|
||||
setRevokeDialogVisible: (value: boolean) => void;
|
||||
|
||||
previewDialogVisible: boolean;
|
||||
setPreviewDialogVisible: (value: boolean) => void;
|
||||
|
||||
disableDialogVisible: boolean;
|
||||
setDisableDialogVisible: (value: boolean) => void;
|
||||
|
||||
resetDialogVisible: boolean;
|
||||
setResetDialogVisible: (value: boolean) => void;
|
||||
|
||||
deleteDialogVisible: boolean;
|
||||
setDeleteDialogVisible: (value: boolean) => void;
|
||||
|
||||
clientsIsLoading: boolean;
|
||||
setClientsIsLoading: (value: boolean) => void;
|
||||
|
||||
consentsIsLoading: boolean;
|
||||
setConsentsIsLoading: (value: boolean) => void;
|
||||
|
||||
clientSecret: string;
|
||||
setClientSecret: (value: string) => void;
|
||||
|
||||
editClient: (clientId: string) => void;
|
||||
|
||||
clients: IClientProps[];
|
||||
|
||||
fetchClients: () => Promise<void>;
|
||||
fetchNextClients: (startIndex: number) => Promise<void>;
|
||||
|
||||
consents: IClientProps[];
|
||||
|
||||
fetchConsents: () => Promise<void>;
|
||||
fetchNextConsents: (startIndex: number) => Promise<void>;
|
||||
|
||||
saveClient: (client: IClientReqDTO) => Promise<void>;
|
||||
|
||||
updateClient: (clientId: string, client: IClientReqDTO) => Promise<void>;
|
||||
|
||||
changeClientStatus: (clientId: string, status: boolean) => Promise<void>;
|
||||
|
||||
regenerateSecret: (clientId: string) => Promise<string | undefined>;
|
||||
|
||||
deleteClient: (clientId: string[]) => Promise<void>;
|
||||
|
||||
revokeClient: (clientId: string[]) => Promise<void>;
|
||||
|
||||
userStore: Nullable<UserStore>;
|
||||
|
||||
currentPage: number;
|
||||
nextPage: Nullable<number>;
|
||||
itemCount: number;
|
||||
|
||||
consentCurrentPage: number;
|
||||
consentNextPage: Nullable<number>;
|
||||
consentItemCount: number;
|
||||
|
||||
selection: string[];
|
||||
setSelection: (clientId: string) => void;
|
||||
|
||||
bufferSelection: IClientProps | null;
|
||||
setBufferSelection: (clientId: string) => void;
|
||||
|
||||
activeClients: string[];
|
||||
setActiveClient: (clientId: string) => void;
|
||||
|
||||
scopes: TScope[];
|
||||
fetchScopes: () => Promise<void>;
|
||||
|
||||
getContextMenuItems: (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo?: boolean,
|
||||
isSettings?: boolean,
|
||||
) => ContextMenuModel[];
|
||||
|
||||
clientList: IClientProps[];
|
||||
isEmptyClientList: boolean;
|
||||
hasNextPage: boolean;
|
||||
consentHasNextPage: boolean;
|
||||
scopeList: TScope[];
|
||||
}
|
||||
|
||||
class OAuthStore implements OAuthStoreProps {
|
||||
userStore: Nullable<UserStore> = null;
|
||||
|
||||
viewAs: ViewAsType = "table";
|
||||
|
||||
currentPage: number = 0;
|
||||
|
||||
nextPage: Nullable<number> = null;
|
||||
|
||||
itemCount: number = 0;
|
||||
|
||||
consentCurrentPage: number = 0;
|
||||
|
||||
consentNextPage: Nullable<number> = null;
|
||||
|
||||
consentItemCount: number = 0;
|
||||
|
||||
infoDialogVisible: boolean = false;
|
||||
|
||||
previewDialogVisible: boolean = false;
|
||||
|
||||
disableDialogVisible: boolean = false;
|
||||
|
||||
deleteDialogVisible: boolean = false;
|
||||
|
||||
resetDialogVisible: boolean = false;
|
||||
|
||||
selection: string[] = [];
|
||||
|
||||
bufferSelection: IClientProps | null = null;
|
||||
|
||||
clients: IClientProps[] = [];
|
||||
|
||||
activeClients: string[] = [];
|
||||
|
||||
scopes: TScope[] = [];
|
||||
|
||||
clientsIsLoading: boolean = true;
|
||||
|
||||
consentsIsLoading: boolean = true;
|
||||
|
||||
clientSecret: string = "";
|
||||
|
||||
consents: IClientProps[] = [];
|
||||
|
||||
isInit: boolean = false;
|
||||
|
||||
revokeDialogVisible: boolean = false;
|
||||
|
||||
constructor(userStore: UserStore) {
|
||||
this.userStore = userStore;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setRevokeDialogVisible = (value: boolean) => {
|
||||
this.revokeDialogVisible = value;
|
||||
};
|
||||
|
||||
setIsInit = (value: boolean) => {
|
||||
this.isInit = value;
|
||||
};
|
||||
|
||||
setViewAs = (value: ViewAsType) => {
|
||||
this.viewAs = value;
|
||||
};
|
||||
|
||||
setInfoDialogVisible = (value: boolean) => {
|
||||
this.infoDialogVisible = value;
|
||||
};
|
||||
|
||||
setPreviewDialogVisible = (value: boolean) => {
|
||||
this.previewDialogVisible = value;
|
||||
};
|
||||
|
||||
setDisableDialogVisible = (value: boolean) => {
|
||||
this.disableDialogVisible = value;
|
||||
};
|
||||
|
||||
setDeleteDialogVisible = (value: boolean) => {
|
||||
this.deleteDialogVisible = value;
|
||||
};
|
||||
|
||||
setResetDialogVisible = (value: boolean) => {
|
||||
this.resetDialogVisible = value;
|
||||
};
|
||||
|
||||
setClientSecret = (value: string) => {
|
||||
this.clientSecret = value;
|
||||
};
|
||||
|
||||
setSelection = (clientId?: string) => {
|
||||
if (!clientId) {
|
||||
this.selection = [];
|
||||
} else if (this.selection.includes(clientId)) {
|
||||
this.selection = this.selection.filter((s) => s !== clientId);
|
||||
} else {
|
||||
this.selection.push(clientId);
|
||||
}
|
||||
};
|
||||
|
||||
setBufferSelection = (clientId: string) => {
|
||||
const client = this.clients.find((c) => c.clientId === clientId);
|
||||
|
||||
if (client) {
|
||||
this.bufferSelection = { ...client, scopes: [...client.scopes] };
|
||||
} else {
|
||||
const consent = this.consents.find((c) => c.clientId === clientId);
|
||||
|
||||
if (consent)
|
||||
this.bufferSelection = { ...consent, scopes: [...consent.scopes] };
|
||||
}
|
||||
};
|
||||
|
||||
setClientsIsLoading = (value: boolean) => {
|
||||
this.clientsIsLoading = value;
|
||||
};
|
||||
|
||||
setConsentsIsLoading = (value: boolean) => {
|
||||
this.consentsIsLoading = value;
|
||||
};
|
||||
|
||||
setActiveClient = (clientId?: string) => {
|
||||
if (!clientId) {
|
||||
this.activeClients = [];
|
||||
} else if (this.activeClients.includes(clientId)) {
|
||||
this.activeClients = this.activeClients.filter((s) => s !== clientId);
|
||||
} else {
|
||||
this.activeClients.push(clientId);
|
||||
}
|
||||
};
|
||||
|
||||
editClient = (clientId: string) => {
|
||||
this.setInfoDialogVisible(false);
|
||||
this.setPreviewDialogVisible(false);
|
||||
|
||||
window?.DocSpace?.navigate(
|
||||
`/portal-settings/developer-tools/oauth/${clientId}`,
|
||||
);
|
||||
};
|
||||
|
||||
fetchClients = async () => {
|
||||
try {
|
||||
this.setClientsIsLoading(true);
|
||||
const clientList: IClientListProps = await getClientList(0, PAGE_LIMIT);
|
||||
|
||||
runInAction(() => {
|
||||
this.clients = [...clientList.data];
|
||||
this.selection = [];
|
||||
this.currentPage = clientList.page;
|
||||
this.nextPage = clientList.next;
|
||||
|
||||
if (clientList.next) {
|
||||
this.itemCount = clientList.data.length + 2;
|
||||
} else {
|
||||
this.itemCount = clientList.data.length;
|
||||
}
|
||||
});
|
||||
this.setClientsIsLoading(false);
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNextClients = async (startIndex: number) => {
|
||||
if (this.clientsIsLoading) return;
|
||||
|
||||
this.setClientsIsLoading(true);
|
||||
|
||||
const page = startIndex / PAGE_LIMIT;
|
||||
|
||||
runInAction(() => {
|
||||
this.currentPage = page + 1;
|
||||
});
|
||||
|
||||
const clientList: IClientListProps = await getClientList(
|
||||
this.nextPage || page,
|
||||
PAGE_LIMIT,
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.currentPage = clientList.page;
|
||||
this.nextPage = clientList.next || null;
|
||||
this.clients = [...this.clients, ...clientList.data];
|
||||
|
||||
this.itemCount += clientList.data.length;
|
||||
});
|
||||
|
||||
this.setClientsIsLoading(false);
|
||||
};
|
||||
|
||||
fetchConsents = async () => {
|
||||
try {
|
||||
this.setClientsIsLoading(true);
|
||||
const consentList = await getConsentList(0, PAGE_LIMIT);
|
||||
|
||||
runInAction(() => {
|
||||
this.consents = [...consentList.consents];
|
||||
this.selection = [];
|
||||
this.consentCurrentPage = consentList.page;
|
||||
this.consentNextPage = consentList.next;
|
||||
|
||||
if (consentList.next) {
|
||||
this.consentItemCount = consentList.data.length + 2;
|
||||
} else {
|
||||
this.consentItemCount = consentList.data.length;
|
||||
}
|
||||
});
|
||||
this.setClientsIsLoading(false);
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNextConsents = async (startIndex: number) => {
|
||||
if (this.consentsIsLoading) return;
|
||||
|
||||
this.setConsentsIsLoading(true);
|
||||
|
||||
const page = startIndex / PAGE_LIMIT;
|
||||
|
||||
runInAction(() => {
|
||||
this.consentCurrentPage = page + 1;
|
||||
});
|
||||
|
||||
const consentList = await getConsentList(this.nextPage || page, PAGE_LIMIT);
|
||||
|
||||
runInAction(() => {
|
||||
this.currentPage = consentList.page;
|
||||
this.nextPage = consentList.next || null;
|
||||
this.consents = [...this.consents, ...consentList.consents];
|
||||
|
||||
this.consentItemCount += consentList.data.length;
|
||||
});
|
||||
|
||||
this.setConsentsIsLoading(false);
|
||||
};
|
||||
|
||||
saveClient = async (client: IClientReqDTO) => {
|
||||
try {
|
||||
const newClient = await addClient(client);
|
||||
|
||||
const creatorDisplayName = this.userStore?.user?.displayName;
|
||||
const creatorAvatar = this.userStore?.user?.avatarSmall;
|
||||
|
||||
runInAction(() => {
|
||||
this.clients = [
|
||||
{ ...newClient, enabled: true, creatorDisplayName, creatorAvatar },
|
||||
...this.clients,
|
||||
];
|
||||
});
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
updateClient = async (clientId: string, client: IClientReqDTO) => {
|
||||
try {
|
||||
await updateClient(clientId, client);
|
||||
|
||||
const idx = this.clients.findIndex((c) => c.clientId === clientId);
|
||||
|
||||
const newClient = { ...this.clients[idx] };
|
||||
|
||||
newClient.name = client.name;
|
||||
newClient.allowedOrigins = client.allowed_origins;
|
||||
newClient.logo = client.logo;
|
||||
newClient.description = client.description;
|
||||
newClient.isPublic = client.is_public;
|
||||
|
||||
if (
|
||||
client.allow_pkce &&
|
||||
!newClient.authenticationMethods.includes(AuthenticationMethod.none)
|
||||
)
|
||||
newClient.authenticationMethods.push(AuthenticationMethod.none);
|
||||
|
||||
if (idx > -1) {
|
||||
runInAction(() => {
|
||||
this.clients[idx] = {
|
||||
...newClient,
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
changeClientStatus = async (clientId: string, status: boolean) => {
|
||||
try {
|
||||
await changeClientStatus(clientId, status);
|
||||
|
||||
const idx = this.clients.findIndex((c) => c.clientId === clientId);
|
||||
|
||||
if (idx > -1) {
|
||||
runInAction(() => {
|
||||
this.clients[idx] = { ...this.clients[idx], enabled: status };
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
regenerateSecret = async (clientId: string) => {
|
||||
try {
|
||||
const { client_secret: clientSecret } = await regenerateSecret(clientId);
|
||||
|
||||
this.setClientSecret(clientSecret);
|
||||
|
||||
return clientSecret;
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
deleteClient = async (clientsId: string[]) => {
|
||||
try {
|
||||
const requests: Promise<void>[] = [];
|
||||
|
||||
clientsId.forEach((id) => {
|
||||
this.setActiveClient(id);
|
||||
requests.push(deleteClient(id));
|
||||
});
|
||||
|
||||
await Promise.all(requests);
|
||||
|
||||
runInAction(() => {
|
||||
this.clients = this.clients.filter(
|
||||
(c) => !clientsId.includes(c.clientId),
|
||||
);
|
||||
});
|
||||
|
||||
this.setActiveClient("");
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchScopes = async () => {
|
||||
try {
|
||||
const scopes = await getScopeList();
|
||||
|
||||
this.scopes = scopes;
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
revokeClient = async (clientsId: string[]) => {
|
||||
try {
|
||||
const requests: Promise<void>[] = [];
|
||||
|
||||
clientsId.forEach((id) => {
|
||||
this.setActiveClient(id);
|
||||
requests.push(revokeUserClient(id));
|
||||
});
|
||||
|
||||
await Promise.all(requests);
|
||||
|
||||
runInAction(() => {
|
||||
this.consents = this.consents.filter(
|
||||
(c) => !clientsId.includes(c.clientId),
|
||||
);
|
||||
});
|
||||
|
||||
this.setActiveClient("");
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
getContextMenuItems = (
|
||||
t: TTranslation,
|
||||
item: IClientProps,
|
||||
isInfo?: boolean,
|
||||
isSettings: boolean = true,
|
||||
) => {
|
||||
const { clientId } = item;
|
||||
|
||||
const isGroupContext = this.selection.length > 1;
|
||||
|
||||
const onShowInfo = () => {
|
||||
this.setBufferSelection(clientId);
|
||||
this.setPreviewDialogVisible(false);
|
||||
this.setInfoDialogVisible(true);
|
||||
this.setDisableDialogVisible(false);
|
||||
this.setDeleteDialogVisible(false);
|
||||
};
|
||||
|
||||
const onRevoke = () => {
|
||||
if (!isGroupContext) this.setBufferSelection(clientId);
|
||||
this.setPreviewDialogVisible(false);
|
||||
this.setInfoDialogVisible(false);
|
||||
this.setRevokeDialogVisible(true);
|
||||
this.setDisableDialogVisible(false);
|
||||
this.setDeleteDialogVisible(false);
|
||||
};
|
||||
|
||||
const onDisable = () => {
|
||||
this.setBufferSelection(clientId);
|
||||
this.setPreviewDialogVisible(false);
|
||||
this.setInfoDialogVisible(false);
|
||||
this.setRevokeDialogVisible(false);
|
||||
this.setDisableDialogVisible(true);
|
||||
this.setDeleteDialogVisible(false);
|
||||
};
|
||||
|
||||
const openOption = {
|
||||
key: "open",
|
||||
icon: ExternalLinkReactSvgUrl,
|
||||
label: t("Files:Open"),
|
||||
onClick: () => window.open(item.websiteUrl, "_blank"),
|
||||
isDisabled: isInfo,
|
||||
};
|
||||
|
||||
const infoOption = {
|
||||
key: "info",
|
||||
icon: SettingsIcon,
|
||||
label: t("Common:Info"),
|
||||
onClick: onShowInfo,
|
||||
isDisabled: isInfo,
|
||||
};
|
||||
|
||||
const revokeOptions = [
|
||||
{
|
||||
key: "revoke",
|
||||
icon: OauthRevokeSvgUrl,
|
||||
label: t("Revoke"),
|
||||
onClick: onRevoke,
|
||||
isDisabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (!isSettings) {
|
||||
const items: ContextMenuModel[] = [];
|
||||
|
||||
if (!isGroupContext) {
|
||||
items.push(openOption);
|
||||
|
||||
if (!isInfo) items.push(infoOption);
|
||||
|
||||
items.push({
|
||||
key: "separator",
|
||||
isSeparator: true,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(...revokeOptions);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
this.setBufferSelection(clientId);
|
||||
this.setPreviewDialogVisible(false);
|
||||
this.setInfoDialogVisible(false);
|
||||
this.setRevokeDialogVisible(false);
|
||||
this.setDisableDialogVisible(false);
|
||||
this.setDeleteDialogVisible(true);
|
||||
};
|
||||
|
||||
const onShowPreview = () => {
|
||||
this.setBufferSelection(clientId);
|
||||
this.setPreviewDialogVisible(true);
|
||||
this.setInfoDialogVisible(false);
|
||||
this.setRevokeDialogVisible(false);
|
||||
this.setDisableDialogVisible(false);
|
||||
this.setDeleteDialogVisible(false);
|
||||
};
|
||||
|
||||
const onEnable = async (status: boolean) => {
|
||||
this.setPreviewDialogVisible(false);
|
||||
this.setInfoDialogVisible(false);
|
||||
this.setRevokeDialogVisible(false);
|
||||
this.setDisableDialogVisible(false);
|
||||
this.setDeleteDialogVisible(false);
|
||||
|
||||
if (isGroupContext) {
|
||||
try {
|
||||
const actions: Promise<void>[] = [];
|
||||
|
||||
this.selection.forEach((s) => {
|
||||
this.setActiveClient(s);
|
||||
actions.push(this.changeClientStatus(s, status));
|
||||
});
|
||||
|
||||
await Promise.all(actions);
|
||||
|
||||
this.setActiveClient("");
|
||||
this.setSelection("");
|
||||
} catch (e) {
|
||||
const err = e as TData;
|
||||
toastr.error(err);
|
||||
}
|
||||
} else {
|
||||
this.setActiveClient(clientId);
|
||||
|
||||
await this.changeClientStatus(clientId, status);
|
||||
|
||||
this.setActiveClient("");
|
||||
this.setSelection("");
|
||||
|
||||
// TODO OAuth, show toast
|
||||
}
|
||||
};
|
||||
|
||||
const editOption = {
|
||||
key: "edit",
|
||||
icon: PencilReactSvgUrl,
|
||||
label: t("Common:Edit"),
|
||||
onClick: () => this.editClient(clientId),
|
||||
};
|
||||
|
||||
const authButtonOption = {
|
||||
key: "auth-button",
|
||||
icon: CodeReactSvgUrl,
|
||||
label: t("AuthButton"),
|
||||
onClick: onShowPreview,
|
||||
};
|
||||
|
||||
const enableOption = {
|
||||
key: "enable",
|
||||
icon: EnableReactSvgUrl,
|
||||
label: t("Common:Enable"),
|
||||
onClick: () => onEnable(true),
|
||||
};
|
||||
|
||||
const disableOption = {
|
||||
key: "disable",
|
||||
icon: RemoveReactSvgUrl,
|
||||
label: t("Common:Disable"),
|
||||
onClick: onDisable,
|
||||
};
|
||||
|
||||
const contextOptions = [
|
||||
{
|
||||
key: "Separator dropdownItem",
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: t("Common:Delete"),
|
||||
icon: DeleteIcon,
|
||||
onClick: () => onDelete(),
|
||||
},
|
||||
];
|
||||
|
||||
if (isGroupContext) {
|
||||
let enabled = false;
|
||||
|
||||
this.selection.forEach((s) => {
|
||||
enabled =
|
||||
enabled ||
|
||||
this.clientList.find((client) => client.clientId === s)?.enabled ||
|
||||
false;
|
||||
});
|
||||
|
||||
if (enabled) {
|
||||
contextOptions.unshift(disableOption);
|
||||
} else {
|
||||
contextOptions.unshift(enableOption);
|
||||
}
|
||||
} else {
|
||||
if (item.enabled) {
|
||||
contextOptions.unshift(disableOption);
|
||||
} else {
|
||||
contextOptions.unshift(enableOption);
|
||||
}
|
||||
|
||||
if (!isInfo) contextOptions.unshift(infoOption);
|
||||
contextOptions.unshift(authButtonOption);
|
||||
contextOptions.unshift(editOption);
|
||||
}
|
||||
|
||||
return contextOptions;
|
||||
};
|
||||
|
||||
get clientList() {
|
||||
return this.clients;
|
||||
}
|
||||
|
||||
get isEmptyClientList() {
|
||||
return this.clientList.length === 0;
|
||||
}
|
||||
|
||||
get hasNextPage() {
|
||||
return !!this.nextPage;
|
||||
}
|
||||
|
||||
get consentHasNextPage() {
|
||||
return !!this.consentNextPage;
|
||||
}
|
||||
|
||||
get scopeList() {
|
||||
return this.scopes;
|
||||
}
|
||||
}
|
||||
|
||||
export default OAuthStore;
|
@ -45,13 +45,13 @@ import { TLogo, TRoomSecurity } from "@docspace/shared/api/rooms/types";
|
||||
import { setDocumentTitle } from "../helpers/utils";
|
||||
|
||||
export type TNavigationPath = {
|
||||
id: number;
|
||||
id: number | string;
|
||||
title: string;
|
||||
isRoom: boolean;
|
||||
roomType: RoomsType;
|
||||
isRootRoom: boolean;
|
||||
shared: boolean;
|
||||
canCopyPublicLink: boolean;
|
||||
roomType?: RoomsType;
|
||||
isRootRoom?: boolean;
|
||||
shared?: boolean;
|
||||
canCopyPublicLink?: boolean;
|
||||
};
|
||||
|
||||
type ExcludeTypes = SettingsStore | Function;
|
||||
@ -115,6 +115,8 @@ class SelectedFolderStore {
|
||||
|
||||
isRoom = false;
|
||||
|
||||
inRoom = false;
|
||||
|
||||
isArchive = false;
|
||||
|
||||
logo: TLogo | null = null;
|
||||
@ -127,9 +129,7 @@ class SelectedFolderStore {
|
||||
|
||||
security: TFolderSecurity | TRoomSecurity | null = null;
|
||||
|
||||
type = null;
|
||||
|
||||
inRoom = false;
|
||||
type: FolderType | null = null;
|
||||
|
||||
isFolder = true;
|
||||
|
||||
@ -349,6 +349,22 @@ class SelectedFolderStore {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
increaseFilesCount = () => {
|
||||
this.filesCount += 1;
|
||||
};
|
||||
|
||||
decreaseFilesCount = () => {
|
||||
this.filesCount -= 1;
|
||||
};
|
||||
|
||||
increaseFoldersCount = () => {
|
||||
this.foldersCount += 1;
|
||||
};
|
||||
|
||||
decreaseFoldersCount = () => {
|
||||
this.foldersCount -= 1;
|
||||
};
|
||||
}
|
||||
|
||||
export default SelectedFolderStore;
|
||||
|
@ -297,4 +297,4 @@ class ThirdPartyStore {
|
||||
}
|
||||
}
|
||||
|
||||
export default new ThirdPartyStore();
|
||||
export default ThirdPartyStore;
|
||||
|
@ -50,7 +50,7 @@ import LdapFormStore from "./LdapFormStore";
|
||||
import FilesStore from "./FilesStore";
|
||||
import SelectedFolderStore from "./SelectedFolderStore";
|
||||
import TreeFoldersStore from "./TreeFoldersStore";
|
||||
import thirdPartyStore from "./ThirdPartyStore";
|
||||
import ThirdPartyStore from "./ThirdPartyStore";
|
||||
import FilesSettingsStore from "./FilesSettingsStore";
|
||||
import FilesActionsStore from "./FilesActionsStore";
|
||||
import MediaViewerDataStore from "./MediaViewerDataStore";
|
||||
@ -81,6 +81,13 @@ import PluginStore from "./PluginStore";
|
||||
import InfoPanelStore from "./InfoPanelStore";
|
||||
import CampaignsStore from "./CampaignsStore";
|
||||
|
||||
import OAuthStore from "./OAuthStore";
|
||||
import FilesSocketStore from "./FilesSocketStore";
|
||||
|
||||
const thirdPartyStore = new ThirdPartyStore();
|
||||
|
||||
const oauthStore = new OAuthStore(userStore);
|
||||
|
||||
const selectedFolderStore = new SelectedFolderStore(settingsStore);
|
||||
|
||||
const pluginStore = new PluginStore(
|
||||
@ -153,6 +160,16 @@ const filesStore = new FilesStore(
|
||||
settingsStore,
|
||||
);
|
||||
|
||||
const filesSocketStore = new FilesSocketStore(
|
||||
settingsStore,
|
||||
clientLoadingStore,
|
||||
selectedFolderStore,
|
||||
treeFoldersStore,
|
||||
infoPanelStore,
|
||||
userStore,
|
||||
filesStore,
|
||||
);
|
||||
|
||||
const mediaViewerDataStore = new MediaViewerDataStore(
|
||||
filesStore,
|
||||
publicRoomStore,
|
||||
@ -320,6 +337,7 @@ const store = {
|
||||
profileActionsStore,
|
||||
|
||||
filesStore,
|
||||
filesSocketStore,
|
||||
|
||||
filesSettingsStore,
|
||||
mediaViewerDataStore,
|
||||
@ -348,6 +366,7 @@ const store = {
|
||||
clientLoadingStore,
|
||||
publicRoomStore,
|
||||
|
||||
oauthStore,
|
||||
pluginStore,
|
||||
storageManagement,
|
||||
campaignsStore,
|
||||
|
@ -25,7 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react": "^18.2.53",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.4",
|
||||
|
4
packages/login/index.d.ts
vendored
4
packages/login/index.d.ts
vendored
@ -27,9 +27,9 @@
|
||||
declare module "*.ico?url" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "*.svg?url" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react": "^18.2.53",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
|
8
packages/login/public/locales/en/Consent.json
Normal file
8
packages/login/public/locales/en/Consent.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Consent": "Consent",
|
||||
"ConsentSubHeader": "{{name}} would like the ability to access the following data in <strong>your DocSpace account</strong>:",
|
||||
"ConsentDescription": "Data shared with <strong>{{displayName}}</strong> will be governed by <strong>{{nameApp}}</strong> <6>privacy policy</6> and <6>terms of service</6>. You can revoke this consent at any time in your DocSpace account settings.",
|
||||
"ToContinue": "To continue to",
|
||||
"SignedInAs": "Signed in as",
|
||||
"NotYou": "Not you?"
|
||||
}
|
@ -16,5 +16,8 @@
|
||||
"SsoSettingsDisabled": "Single sign-on is disabled",
|
||||
"SsoSettingsEmptyToken": "Authentication token could not be found",
|
||||
"SsoSettingsNotValidToken": "Invalid authentication token",
|
||||
"SsoSettingsUserTerminated": "This user is disabled"
|
||||
"SsoSettingsUserTerminated": "This user is disabled",
|
||||
"OAuthApplicationEmpty": "Application could not be found",
|
||||
"OAuthApplicationDisabled": "This application is disabled",
|
||||
"OAuthClientEmpty": "Client id could not be found"
|
||||
}
|
||||
|
4
packages/login/public/locales/en/TenantList.json
Normal file
4
packages/login/public/locales/en/TenantList.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"BackToSignIn": "Back to sign in",
|
||||
"MorePortals": "You have more than one accounts. Please choose one of them"
|
||||
}
|
52
packages/login/src/app/(root)/consent/page.tsx
Normal file
52
packages/login/src/app/(root)/consent/page.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { IClientProps } from "@docspace/shared/utils/oauth/types";
|
||||
|
||||
import Consent from "@/components/Consent";
|
||||
import { getOAuthClient, getScopeList, getUser } from "@/utils/actions";
|
||||
|
||||
async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string };
|
||||
}) {
|
||||
const clientId = searchParams.clientId ?? searchParams.client_id;
|
||||
const [client, scopes, user] = await Promise.all([
|
||||
getOAuthClient(clientId),
|
||||
getScopeList(),
|
||||
getUser(),
|
||||
]);
|
||||
|
||||
if (!client || (client && !("clientId" in client)) || !scopes || !user)
|
||||
return "";
|
||||
|
||||
return (
|
||||
<Consent client={client as IClientProps} scopes={scopes} user={user} />
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
@ -24,6 +24,7 @@
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import React from "react";
|
||||
import { cookies } from "next/headers";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
@ -82,7 +83,7 @@ export default async function Layout({
|
||||
<GreetingContainer
|
||||
greetingSettings={objectSettings?.greetingSettings}
|
||||
/>
|
||||
<FormWrapper id="login-form">{children}</FormWrapper>
|
||||
{children}
|
||||
</ColorTheme>
|
||||
</LoginContent>
|
||||
</Scrollbar>
|
||||
|
@ -24,29 +24,42 @@
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { getSettings } from "@/utils/actions";
|
||||
import { getOAuthClient, getSettings } from "@/utils/actions";
|
||||
import Login from "@/components/Login";
|
||||
import LoginForm from "@/components/LoginForm";
|
||||
import ThirdParty from "@/components/ThirdParty";
|
||||
import RecoverAccess from "@/components/RecoverAccess";
|
||||
import Register from "@/components/Register";
|
||||
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
|
||||
|
||||
async function Page() {
|
||||
const settings = await getSettings();
|
||||
async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string };
|
||||
}) {
|
||||
const clientId = searchParams.client_id;
|
||||
|
||||
const [settings, client] = await Promise.all([
|
||||
getSettings(),
|
||||
clientId ? getOAuthClient(clientId) : undefined,
|
||||
]);
|
||||
|
||||
return (
|
||||
<FormWrapper id="login-form">
|
||||
<Login>
|
||||
{settings && typeof settings !== "string" && (
|
||||
<>
|
||||
<LoginForm
|
||||
hashSettings={settings?.passwordHash}
|
||||
cookieSettingsEnabled={settings?.cookieSettingsEnabled}
|
||||
clientId={clientId}
|
||||
client={client}
|
||||
reCaptchaPublicKey={settings?.recaptchaPublicKey}
|
||||
reCaptchaType={settings?.recaptchaType}
|
||||
/>
|
||||
<ThirdParty />
|
||||
{!clientId && <ThirdParty />}
|
||||
{settings.enableAdmMess && <RecoverAccess />}
|
||||
{settings.enabledJoin && (
|
||||
{settings.enabledJoin && !clientId && (
|
||||
<Register
|
||||
id="login_register"
|
||||
enabledJoin
|
||||
@ -58,6 +71,7 @@ async function Page() {
|
||||
</>
|
||||
)}
|
||||
</Login>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
23
packages/login/src/app/(root)/tenant-list/page.tsx
Normal file
23
packages/login/src/app/(root)/tenant-list/page.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import TenantList from "@/components/TenantList";
|
||||
import { getSettings } from "@/utils/actions";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { [key: string]: string };
|
||||
}) {
|
||||
const settings = await getSettings();
|
||||
|
||||
const { portals } = JSON.parse(searchParams.portals);
|
||||
const clientId = searchParams.clientId;
|
||||
|
||||
if (typeof settings !== "object") return;
|
||||
|
||||
return (
|
||||
<TenantList
|
||||
portals={portals}
|
||||
clientId={clientId}
|
||||
baseDomain={settings.baseDomain}
|
||||
/>
|
||||
);
|
||||
}
|
@ -33,7 +33,7 @@ import { LANGUAGE, SYSTEM_THEME_KEY } from "@docspace/shared/constants";
|
||||
|
||||
import StyledComponentsRegistry from "@/utils/registry";
|
||||
import { Providers } from "@/providers";
|
||||
import { getColorTheme, getSettings } from "@/utils/actions";
|
||||
import { getColorTheme, getConfig, getSettings } from "@/utils/actions";
|
||||
|
||||
import "../styles/globals.scss";
|
||||
|
||||
@ -62,9 +62,10 @@ export default async function RootLayout({
|
||||
|
||||
const startOtherOperationsDate = new Date();
|
||||
|
||||
const [settings, colorTheme] = await Promise.all([
|
||||
const [settings, colorTheme, config] = await Promise.all([
|
||||
getSettings(),
|
||||
getColorTheme(),
|
||||
getConfig(),
|
||||
]);
|
||||
|
||||
timers.otherOperations =
|
||||
@ -73,6 +74,7 @@ export default async function RootLayout({
|
||||
if (settings === "access-restricted") redirectUrl = `/${settings}`;
|
||||
|
||||
if (settings === "portal-not-found") {
|
||||
const hdrs = headers();
|
||||
const config = await (
|
||||
await fetch(`${baseUrl}/static/scripts/config.json`)
|
||||
).json();
|
||||
@ -132,7 +134,6 @@ export default async function RootLayout({
|
||||
systemTheme: systemTheme?.value as ThemeKeys,
|
||||
}}
|
||||
redirectURL={redirectUrl}
|
||||
timers={timers}
|
||||
>
|
||||
<Toast isSSR />
|
||||
{children}
|
||||
|
251
packages/login/src/components/Consent.tsx
Normal file
251
packages/login/src/components/Consent.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
|
||||
import { Button, ButtonSize } from "@docspace/shared/components/button";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarRole,
|
||||
AvatarSize,
|
||||
} from "@docspace/shared/components/avatar";
|
||||
import { deleteCookie } from "@docspace/shared/utils/cookie";
|
||||
import { IClientProps, TScope } from "@docspace/shared/utils/oauth/types";
|
||||
import { TUser } from "@docspace/shared/api/people/types";
|
||||
import api from "@docspace/shared/api";
|
||||
|
||||
import OAuthClientInfo from "./ConsentInfo";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledDescriptionContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledUserContainer = styled.div`
|
||||
width: 100%;
|
||||
|
||||
padding-top: 16px;
|
||||
|
||||
border-top: 1px solid
|
||||
${(props) => props.theme.oauth.infoDialog.separatorColor};
|
||||
|
||||
.block {
|
||||
height: 40px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface IConsentProps {
|
||||
client: IClientProps;
|
||||
scopes: TScope[];
|
||||
user: TUser;
|
||||
}
|
||||
|
||||
const Consent = ({ client, scopes, user }: IConsentProps) => {
|
||||
const { t } = useTranslation(["Consent", "Common"]);
|
||||
const router = useRouter();
|
||||
|
||||
const [isAllowRunning, setIsAllowRunning] = React.useState(false);
|
||||
const [isDenyRunning, setIsDenyRunning] = React.useState(false);
|
||||
|
||||
const onAllowClick = async () => {
|
||||
if (!("clientId" in client)) return;
|
||||
|
||||
if (isAllowRunning || isDenyRunning) return;
|
||||
|
||||
setIsAllowRunning(true);
|
||||
|
||||
const clientId = client.clientId;
|
||||
|
||||
let clientState = "";
|
||||
|
||||
const scope = client.scopes;
|
||||
|
||||
const cookie = document.cookie.split(";");
|
||||
|
||||
cookie.forEach((c) => {
|
||||
if (c.includes("client_state"))
|
||||
clientState = c.replace("client_state=", "").trim();
|
||||
});
|
||||
|
||||
await api.oauth.onOAuthSubmit(clientId, clientState, scope);
|
||||
|
||||
setIsAllowRunning(false);
|
||||
};
|
||||
|
||||
const onDenyClick = async () => {
|
||||
if (!("clientId" in client)) return;
|
||||
|
||||
if (isAllowRunning || isDenyRunning) return;
|
||||
|
||||
setIsDenyRunning(true);
|
||||
|
||||
const clientId = client.clientId;
|
||||
|
||||
let clientState = "";
|
||||
|
||||
const cookie = document.cookie.split(";");
|
||||
|
||||
cookie.forEach((c) => {
|
||||
if (c.includes("client_state"))
|
||||
clientState = c.replace("client_state=", "").trim();
|
||||
});
|
||||
|
||||
deleteCookie("client_state");
|
||||
|
||||
await api.oauth.onOAuthCancel(clientId, clientState);
|
||||
|
||||
setIsDenyRunning(false);
|
||||
};
|
||||
|
||||
const onChangeUserClick = async () => {
|
||||
await api.user.logout();
|
||||
|
||||
router.push(`/?client_id=${client.clientId}&type=oauth2`);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormWrapper>
|
||||
<OAuthClientInfo
|
||||
name={client.name}
|
||||
logo={client.logo}
|
||||
websiteUrl={client.websiteUrl}
|
||||
isConsentScreen
|
||||
/>
|
||||
|
||||
<ScopeList
|
||||
t={t}
|
||||
selectedScopes={client.scopes || []}
|
||||
scopes={scopes || []}
|
||||
/>
|
||||
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
onClick={onAllowClick}
|
||||
label={"Allow"}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
primary
|
||||
isDisabled={isDenyRunning}
|
||||
isLoading={isAllowRunning}
|
||||
/>
|
||||
<Button
|
||||
onClick={onDenyClick}
|
||||
label={"Deny"}
|
||||
size={ButtonSize.normal}
|
||||
scale
|
||||
isDisabled={isAllowRunning}
|
||||
isLoading={isDenyRunning}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
<StyledDescriptionContainer>
|
||||
<Text fontWeight={400} fontSize={"13px"} lineHeight={"20px"}>
|
||||
<Trans t={t} i18nKey={"ConsentDescription"} ns="Consent">
|
||||
Data shared with {{ displayName: user.displayName }} will be
|
||||
governed by {{ nameApp: client.name }}
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type={LinkType.page}
|
||||
isHovered={false}
|
||||
href={client.policyUrl}
|
||||
target={LinkTarget.blank}
|
||||
noHover
|
||||
>
|
||||
privacy policy
|
||||
</Link>
|
||||
and
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type={LinkType.page}
|
||||
isHovered={false}
|
||||
href={client.termsUrl}
|
||||
target={LinkTarget.blank}
|
||||
noHover
|
||||
>
|
||||
terms of service
|
||||
</Link>
|
||||
. You can revoke this consent at any time in your DocSpace account
|
||||
settings.
|
||||
</Trans>
|
||||
</Text>
|
||||
</StyledDescriptionContainer>
|
||||
<StyledUserContainer>
|
||||
<div className="block">
|
||||
<Avatar
|
||||
size={AvatarSize.min}
|
||||
role={AvatarRole.user}
|
||||
source={user.avatarSmall || ""}
|
||||
/>
|
||||
<div className="user-info">
|
||||
<Text lineHeight={"20px"}>
|
||||
{t("SignedInAs")} {user.email}
|
||||
</Text>
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type={LinkType.action}
|
||||
isHovered={false}
|
||||
noHover
|
||||
lineHeight={"20px"}
|
||||
onClick={onChangeUserClick}
|
||||
>
|
||||
{t("NotYou")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</StyledUserContainer>
|
||||
</FormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Consent;
|
111
packages/login/src/components/ConsentInfo.tsx
Normal file
111
packages/login/src/components/ConsentInfo.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
|
||||
|
||||
const StyledOAuthContainer = styled.div`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
margin-bottom: 32px;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
cursor: normal;
|
||||
}
|
||||
`;
|
||||
|
||||
interface IOAuthClientInfo {
|
||||
name: string;
|
||||
logo: string;
|
||||
websiteUrl: string;
|
||||
|
||||
isConsentScreen?: boolean;
|
||||
}
|
||||
|
||||
const OAuthClientInfo = ({
|
||||
name,
|
||||
logo,
|
||||
websiteUrl,
|
||||
|
||||
isConsentScreen,
|
||||
}: IOAuthClientInfo) => {
|
||||
const { t } = useTranslation(["Consent", "Common"]);
|
||||
|
||||
return (
|
||||
<StyledOAuthContainer>
|
||||
{!isConsentScreen && (
|
||||
<Text
|
||||
className="row"
|
||||
fontWeight={600}
|
||||
fontSize="16px"
|
||||
lineHeight="22px"
|
||||
>
|
||||
{t("Common:LoginButton")}
|
||||
</Text>
|
||||
)}
|
||||
<img src={logo} alt={"client-logo"} />
|
||||
<Text
|
||||
className="row"
|
||||
fontWeight={isConsentScreen ? 400 : 600}
|
||||
fontSize="16px"
|
||||
lineHeight="22px"
|
||||
>
|
||||
{isConsentScreen ? (
|
||||
<Trans t={t} i18nKey={"ConsentSubHeader"} ns="Consent">
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type={LinkType.page}
|
||||
isHovered={false}
|
||||
href={websiteUrl}
|
||||
target={LinkTarget.blank}
|
||||
noHover
|
||||
fontWeight={600}
|
||||
fontSize="16px"
|
||||
>
|
||||
{name}
|
||||
</Link>{" "}
|
||||
would like the ability to access the following data in{" "}
|
||||
<strong>your DocSpace account</strong>:
|
||||
</Trans>
|
||||
) : (
|
||||
<>
|
||||
{t("Consent:ToContinue")}{" "}
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type={LinkType.page}
|
||||
isHovered={false}
|
||||
href={websiteUrl}
|
||||
target={LinkTarget.blank}
|
||||
noHover
|
||||
fontWeight={600}
|
||||
fontSize="16px"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</StyledOAuthContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthClientInfo;
|
@ -29,7 +29,7 @@
|
||||
|
||||
import React, { useLayoutEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useTheme } from "styled-components";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
@ -48,6 +48,7 @@ const GreetingContainer = ({ greetingSettings }: GreetingContainersProps) => {
|
||||
const logoUrl = getLogoUrl(WhiteLabelLogoType.LoginPage, !theme.isBase);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [invitationLinkData, setInvitationLinkData] = useState({
|
||||
email: "",
|
||||
@ -84,7 +85,9 @@ const GreetingContainer = ({ greetingSettings }: GreetingContainersProps) => {
|
||||
textAlign="center"
|
||||
className="greeting-title"
|
||||
>
|
||||
{greetingSettings}
|
||||
{pathname === "/tenant-list"
|
||||
? "Choose your portal"
|
||||
: greetingSettings}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
@ -35,8 +35,13 @@ import { Text } from "@docspace/shared/components/text";
|
||||
import { combineUrl } from "@docspace/shared/utils/combineUrl";
|
||||
import ErrorContainer from "@docspace/shared/components/error-container/ErrorContainer";
|
||||
|
||||
import { getMessageFromKey, getMessageKeyTranslate } from "@/utils";
|
||||
import {
|
||||
getMessageFromKey,
|
||||
getMessageKeyTranslate,
|
||||
getOAuthMessageKeyTranslation,
|
||||
} from "@/utils";
|
||||
import { PRODUCT_NAME } from "@docspace/shared/constants";
|
||||
import { OAuth2ErrorKey } from "@/utils/enums";
|
||||
|
||||
const homepage = "/";
|
||||
|
||||
@ -62,7 +67,14 @@ const InvalidError = ({ match }: InvalidErrorProps) => {
|
||||
}, [router]);
|
||||
|
||||
const message = getMessageFromKey(match?.messageKey ? +match.messageKey : 1);
|
||||
const errorTitle = match?.messageKey
|
||||
const oauthError = getOAuthMessageKeyTranslation(
|
||||
t,
|
||||
match?.oauthMessageKey as OAuth2ErrorKey | undefined,
|
||||
);
|
||||
|
||||
const errorTitle = oauthError
|
||||
? oauthError
|
||||
: match?.messageKey
|
||||
? getMessageKeyTranslate(t, message)
|
||||
: t("Common:ExpiredLink");
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user