feat: Open doc links in a new tab (#22613)
## Description When user clicks on a docs link, open the docs in a new tab instead of the omnibar. Fixes #22409 Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video ## Type of change - New feature (non-breaking change which adds functionality) ## How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Provide instructions, so we can reproduce. > Please also list any relevant details for your test configuration. > Delete anything that is not important - Manual - Jest - Cypress ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test
This commit is contained in:
parent
b3d9e56664
commit
3408729df9
|
|
@ -13,12 +13,11 @@ describe("Omnibar functionality test cases", () => {
|
|||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
it("1. Bug #15104 The Data is not displayed in Omnibar after clicking on learn more link from property pane", function () {
|
||||
it("1. Docs tab opens after clicking on learn more link from property pane", function () {
|
||||
cy.dragAndDropToCanvas("audiowidget", { x: 300, y: 500 });
|
||||
cy.xpath('//span[text()="Learn more"]').click();
|
||||
cy.get(locators._omnibarDescription).scrollTo("top");
|
||||
cy.get(omnibar.openDocumentationLink);
|
||||
cy.get("body").click(0, 0);
|
||||
ObjectsRegistry.AggregateHelper.AssertNewTabOpened(() => {
|
||||
cy.xpath('//span[text()="Learn more"]').click();
|
||||
});
|
||||
});
|
||||
|
||||
it("2.Verify omnibar is present across all pages and validate its fields", function () {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ describe("API Panel Test Functionality ", function () {
|
|||
cy.get("body").click(0, 0);
|
||||
ee.ExpandCollapseEntity("Queries/JS");
|
||||
ee.ActionContextMenuByEntityName("FirstAPI", "Copy to page", "SecondPage");
|
||||
// click on learn how link
|
||||
cy.get(".t--learn-how-apis-link").click();
|
||||
// this should open in a global search modal
|
||||
cy.get(commonlocators.globalSearchModal);
|
||||
ObjectsRegistry.AggregateHelper.AssertNewTabOpened(() => {
|
||||
// click on learn how link
|
||||
cy.get(".t--learn-how-apis-link").click();
|
||||
});
|
||||
cy.get("body").click(0, 0);
|
||||
ee.ActionContextMenuByEntityName("FirstAPICopy", "Move to page", "Page1");
|
||||
cy.wait(2000);
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ describe("Check datasource doc links", function () {
|
|||
cy.get("@dsName").then(($dsName) => {
|
||||
dsName = $dsName;
|
||||
_.dataSources.CreateQueryAfterDSSaved();
|
||||
_.agHelper.GetNClick(_.dataSources._queryDoc);
|
||||
_.agHelper.AssertElementVisible(_.dataSources._globalSearchModal);
|
||||
_.agHelper.AssertElementVisible(
|
||||
_.dataSources._globalSearchInput("PostgreSQL"),
|
||||
);
|
||||
_.agHelper.AssertNewTabOpened(() => {
|
||||
_.agHelper.GetNClick(_.dataSources._queryDoc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -21,11 +19,9 @@ describe("Check datasource doc links", function () {
|
|||
cy.get("@dsName").then(($dsName) => {
|
||||
dsName = $dsName;
|
||||
_.dataSources.CreateQueryAfterDSSaved();
|
||||
_.agHelper.GetNClick(_.dataSources._queryDoc);
|
||||
_.agHelper.AssertElementVisible(_.dataSources._globalSearchModal);
|
||||
_.agHelper.AssertElementVisible(
|
||||
_.dataSources._globalSearchInput("MongoDB"),
|
||||
);
|
||||
_.agHelper.AssertNewTabOpened(() => {
|
||||
_.agHelper.GetNClick(_.dataSources._queryDoc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -34,11 +30,9 @@ describe("Check datasource doc links", function () {
|
|||
cy.get("@dsName").then(($dsName) => {
|
||||
dsName = $dsName;
|
||||
_.dataSources.CreateQueryAfterDSSaved();
|
||||
_.agHelper.GetNClick(_.dataSources._queryDoc);
|
||||
_.agHelper.AssertElementVisible(_.dataSources._globalSearchModal);
|
||||
_.agHelper.AssertElementVisible(
|
||||
_.dataSources._globalSearchInput("MySQL"),
|
||||
);
|
||||
_.agHelper.AssertNewTabOpened(() => {
|
||||
_.agHelper.GetNClick(_.dataSources._queryDoc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,18 @@ export class AggregateHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public AssertNewTabOpened(openTabFunc: () => void) {
|
||||
cy.window().then((win) => {
|
||||
cy.spy(win, "open").as("windowOpen");
|
||||
openTabFunc();
|
||||
cy.get("@windowOpen").should(
|
||||
"be.calledWith",
|
||||
Cypress.sinon.match.string,
|
||||
"_blank",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//Not used:
|
||||
// private xPathToCss(xpath: string) {
|
||||
// return xpath
|
||||
|
|
|
|||
28
app/client/src/constants/DocumentationLinks.ts
Normal file
28
app/client/src/constants/DocumentationLinks.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import AnalyticsUtil from "../utils/AnalyticsUtil";
|
||||
|
||||
export enum DocsLink {
|
||||
CAPTURE_DATA = "CAPTURE_DATA",
|
||||
WHITELIST_IP = "WHITELIST_IP",
|
||||
CONNECT_DATA = "CONNECT_DATA",
|
||||
QUERY = "QUERY",
|
||||
}
|
||||
|
||||
const LinkData: Record<DocsLink, string> = {
|
||||
CONNECT_DATA:
|
||||
"https://docs.appsmith.com/core-concepts/connecting-to-data-sources",
|
||||
QUERY:
|
||||
"https://docs.appsmith.com/core-concepts/connecting-to-data-sources#docusaurus_skipToContent_fallback",
|
||||
WHITELIST_IP:
|
||||
"https://docs.appsmith.com/core-concepts/connecting-to-data-sources/connecting-to-databases",
|
||||
CAPTURE_DATA:
|
||||
"https://docs.appsmith.com/core-concepts/data-access-and-binding/capturing-data-write",
|
||||
};
|
||||
|
||||
export const openDoc = (type: DocsLink, link?: string, subType?: string) => {
|
||||
let linkToOpen = LinkData[type];
|
||||
if (link && link.length) {
|
||||
linkToOpen = link;
|
||||
}
|
||||
AnalyticsUtil.logEvent("OPEN_DOCS", { source: type, queryType: subType });
|
||||
window.open(linkToOpen, "_blank");
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
HTTP_METHOD_OPTIONS,
|
||||
API_EDITOR_TABS,
|
||||
HTTP_METHOD_OPTIONS,
|
||||
} from "constants/ApiEditorConstants/CommonApiConstants";
|
||||
import { GRAPHQL_HTTP_METHOD_OPTIONS } from "constants/ApiEditorConstants/GraphQLEditorConstants";
|
||||
import styled from "styled-components";
|
||||
|
|
@ -11,10 +11,6 @@ import FormRow from "components/editorComponents/FormRow";
|
|||
import type { PaginationField, SuggestedWidget } from "api/ActionAPI";
|
||||
import type { Action, PaginationType } from "entities/Action";
|
||||
import { isGraphqlPlugin, SlashCommand } from "entities/Action";
|
||||
import {
|
||||
setGlobalSearchQuery,
|
||||
toggleShowGlobalSearchModal,
|
||||
} from "actions/globalSearchActions";
|
||||
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
|
||||
import ApiResponseView from "components/editorComponents/ApiResponseView";
|
||||
import EmbeddedDatasourcePathField from "components/editorComponents/form/fields/EmbeddedDatasourcePathField";
|
||||
|
|
@ -48,7 +44,6 @@ import {
|
|||
createMessage,
|
||||
WIDGET_BIND_HELP,
|
||||
} from "@appsmith/constants/messages";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import CloseEditor from "components/editorComponents/CloseEditor";
|
||||
import { useParams } from "react-router";
|
||||
import DataSourceList from "./ApiRightPane";
|
||||
|
|
@ -67,11 +62,14 @@ import {
|
|||
hasExecuteActionPermission,
|
||||
hasManageActionPermission,
|
||||
} from "@appsmith/utils/permissionHelpers";
|
||||
import { executeCommandAction } from "actions/apiPaneActions";
|
||||
import {
|
||||
executeCommandAction,
|
||||
setApiPaneConfigSelectedTabIndex,
|
||||
} from "actions/apiPaneActions";
|
||||
import { getApiPaneConfigSelectedTabIndex } from "selectors/apiPaneSelectors";
|
||||
import { setApiPaneConfigSelectedTabIndex } from "actions/apiPaneActions";
|
||||
import type { AutoGeneratedHeader } from "./helpers";
|
||||
import { showDebuggerFlag } from "selectors/debuggerSelectors";
|
||||
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
|
||||
|
||||
const Form = styled.form`
|
||||
position: relative;
|
||||
|
|
@ -706,9 +704,7 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
|
|||
const theme = EditorTheme.LIGHT;
|
||||
const handleClickLearnHow = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setGlobalSearchQuery("capturing data"));
|
||||
dispatch(toggleShowGlobalSearchModal());
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "LEARN_HOW_DATASOURCE" });
|
||||
openDoc(DocsLink.CAPTURE_DATA);
|
||||
};
|
||||
|
||||
function handleSearchSnippetClick() {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import type { InjectedFormProps } from "redux-form";
|
|||
import { reduxForm } from "redux-form";
|
||||
import { APPSMITH_IP_ADDRESSES } from "constants/DatasourceEditorConstants";
|
||||
import { getAppsmithConfigs } from "@appsmith/configs";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { convertArrayToSentence } from "utils/helpers";
|
||||
import { PluginType } from "entities/Action";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
|
|
@ -35,12 +34,12 @@ import Debugger, {
|
|||
} from "./Debugger";
|
||||
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
|
||||
import { showDebuggerFlag } from "selectors/debuggerSelectors";
|
||||
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
|
||||
|
||||
const { cloudHosting } = getAppsmithConfigs();
|
||||
|
||||
interface DatasourceDBEditorProps extends JSONtoFormProps {
|
||||
setDatasourceViewMode: (viewMode: boolean) => void;
|
||||
openOmnibarReadMore: (text: string) => void;
|
||||
datasourceId: string;
|
||||
applicationId: string;
|
||||
pageId: string;
|
||||
|
|
@ -101,10 +100,8 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
|
|||
});
|
||||
};
|
||||
|
||||
openOmnibarReadMore = () => {
|
||||
const { openOmnibarReadMore } = this.props;
|
||||
openOmnibarReadMore("connect to databases");
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "READ_MORE_DATASOURCE" });
|
||||
openDocumentation = () => {
|
||||
openDoc(DocsLink.WHITELIST_IP);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
@ -188,7 +185,7 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
|
|||
<span>{`Whitelist the IP ${convertArrayToSentence(
|
||||
APPSMITH_IP_ADDRESSES,
|
||||
)} on your database instance to connect to it. `}</span>
|
||||
<a onClick={this.openOmnibarReadMore}>
|
||||
<a onClick={this.openDocumentation}>
|
||||
{"Learn more "}
|
||||
<StyledOpenDocsIcon icon="document-open" />
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ import RestAPIDatasourceForm from "./RestAPIDatasourceForm";
|
|||
import type { Datasource } from "entities/Datasource";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
|
||||
import { setGlobalSearchQuery } from "actions/globalSearchActions";
|
||||
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
|
||||
import { DatasourceComponentTypes } from "api/PluginApi";
|
||||
import DatasourceSaasForm from "../SaaSEditor/DatasourceForm";
|
||||
|
||||
|
|
@ -166,7 +164,6 @@ class DataSourceEditor extends React.Component<Props> {
|
|||
isNewDatasource,
|
||||
isSaving,
|
||||
isTesting,
|
||||
openOmnibarReadMore,
|
||||
pageId,
|
||||
pluginId,
|
||||
pluginImages,
|
||||
|
|
@ -189,7 +186,6 @@ class DataSourceEditor extends React.Component<Props> {
|
|||
isNewDatasource={isNewDatasource}
|
||||
isSaving={isSaving}
|
||||
isTesting={isTesting}
|
||||
openOmnibarReadMore={openOmnibarReadMore}
|
||||
pageId={pageId}
|
||||
pluginImage={pluginImages[pluginId]}
|
||||
pluginType={pluginType}
|
||||
|
|
@ -203,7 +199,6 @@ class DataSourceEditor extends React.Component<Props> {
|
|||
export interface DatasourcePaneFunctions {
|
||||
switchDatasource: (id: string) => void;
|
||||
setDatasourceViewMode: (viewMode: boolean) => void;
|
||||
openOmnibarReadMore: (text: string) => void;
|
||||
discardTempDatasource: () => void;
|
||||
deleteTempDSFromDraft: () => void;
|
||||
toggleSaveActionFlag: (flag: boolean) => void;
|
||||
|
|
@ -542,10 +537,6 @@ const mapDispatchToProps = (
|
|||
},
|
||||
setDatasourceViewMode: (viewMode: boolean) =>
|
||||
dispatch(setDatasourceViewMode(viewMode)),
|
||||
openOmnibarReadMore: (text: string) => {
|
||||
dispatch(setGlobalSearchQuery(text));
|
||||
dispatch(toggleShowGlobalSearchModal());
|
||||
},
|
||||
discardTempDatasource: () => dispatch(removeTempDatasource()),
|
||||
deleteTempDSFromDraft: () => dispatch(deleteTempDSFromDraft()),
|
||||
toggleSaveActionFlag: (flag) => dispatch(toggleSaveActionFlag(flag)),
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { Button, Category, getTypographyByKey, Size } from "design-system-old";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import styled from "styled-components";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { INTEGRATION_EDITOR_MODES, INTEGRATION_TABS } from "constants/routes";
|
||||
import history from "utils/history";
|
||||
import {
|
||||
setGlobalSearchQuery,
|
||||
toggleShowGlobalSearchModal,
|
||||
} from "actions/globalSearchActions";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import type { WidgetType } from "constants/WidgetConstants";
|
||||
import { integrationEditorURL } from "RouteBuilder";
|
||||
import { getCurrentPageId } from "selectors/editorSelectors";
|
||||
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
color: ${(props) => props.theme.colors.propertyPane.ctaTextColor};
|
||||
|
|
@ -54,15 +51,7 @@ type ConnectDataCTAProps = {
|
|||
};
|
||||
|
||||
function ConnectDataCTA(props: ConnectDataCTAProps) {
|
||||
const dispatch = useDispatch();
|
||||
const pageId: string = useSelector(getCurrentPageId);
|
||||
const openHelpModal = useCallback(() => {
|
||||
dispatch(setGlobalSearchQuery("Connecting to Data Sources"));
|
||||
dispatch(toggleShowGlobalSearchModal());
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", {
|
||||
source: "PROPERTY_PANE_CONNECT_DATA",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onClick = () => {
|
||||
const { widgetId, widgetTitle, widgetType } = props;
|
||||
|
|
@ -93,7 +82,7 @@ function ConnectDataCTA(props: ConnectDataCTAProps) {
|
|||
/>
|
||||
<Button
|
||||
category={Category.secondary}
|
||||
onClick={openHelpModal}
|
||||
onClick={() => openDoc(DocsLink.CONNECT_DATA)}
|
||||
tabIndex={0}
|
||||
tag="button"
|
||||
text="Learn more"
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ import Resizable, {
|
|||
} from "components/editorComponents/Debugger/Resizer";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import CloseEditor from "components/editorComponents/CloseEditor";
|
||||
import { setGlobalSearchQuery } from "actions/globalSearchActions";
|
||||
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
|
||||
import EntityDeps from "components/editorComponents/Debugger/EntityDependecies";
|
||||
import {
|
||||
checkIfSectionCanRender,
|
||||
|
|
@ -60,20 +58,20 @@ import {
|
|||
updateEvaluatedSectionConfig,
|
||||
} from "components/formControls/utils";
|
||||
import {
|
||||
ACTION_EDITOR_REFRESH,
|
||||
ACTION_EXECUTION_MESSAGE,
|
||||
ACTION_RUN_BUTTON_MESSAGE_FIRST_HALF,
|
||||
ACTION_RUN_BUTTON_MESSAGE_SECOND_HALF,
|
||||
CREATE_NEW_DATASOURCE,
|
||||
createMessage,
|
||||
DEBUGGER_ERRORS,
|
||||
DEBUGGER_LOGS,
|
||||
DOCUMENTATION,
|
||||
DOCUMENTATION_TOOLTIP,
|
||||
INSPECT_ENTITY,
|
||||
ACTION_EXECUTION_MESSAGE,
|
||||
UNEXPECTED_ERROR,
|
||||
NO_DATASOURCE_FOR_QUERY,
|
||||
ACTION_EDITOR_REFRESH,
|
||||
INVALID_FORM_CONFIGURATION,
|
||||
ACTION_RUN_BUTTON_MESSAGE_FIRST_HALF,
|
||||
ACTION_RUN_BUTTON_MESSAGE_SECOND_HALF,
|
||||
CREATE_NEW_DATASOURCE,
|
||||
DEBUGGER_ERRORS,
|
||||
NO_DATASOURCE_FOR_QUERY,
|
||||
UNEXPECTED_ERROR,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { useParams } from "react-router";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
|
|
@ -102,15 +100,15 @@ import { EDITOR_TABS } from "constants/QueryEditorConstants";
|
|||
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
|
||||
import { isValidFormConfig } from "reducers/evaluationReducers/formEvaluationReducer";
|
||||
import {
|
||||
responseTabComponent,
|
||||
InlineButton,
|
||||
apiReactJsonProps,
|
||||
CancelRequestButton,
|
||||
LoadingOverlayContainer,
|
||||
handleCancelActionExecution,
|
||||
InlineButton,
|
||||
LoadingOverlayContainer,
|
||||
responseTabComponent,
|
||||
ResponseTabErrorContainer,
|
||||
ResponseTabErrorContent,
|
||||
ResponseTabErrorDefaultMessage,
|
||||
apiReactJsonProps,
|
||||
} from "components/editorComponents/ApiResponseView";
|
||||
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
|
||||
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
|
|
@ -145,6 +143,7 @@ import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
|||
import type { SourceEntity } from "entities/AppsmithConsole";
|
||||
import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole";
|
||||
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
|
||||
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
|
||||
import { AIWindow } from "@appsmith/components/editorComponents/GPT";
|
||||
|
||||
const QueryFormContainer = styled.form`
|
||||
|
|
@ -613,13 +612,7 @@ export function EditorJSONtoForm(props: Props) {
|
|||
|
||||
const handleDocumentationClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const query = plugin?.name || "Connecting to datasources";
|
||||
dispatch(setGlobalSearchQuery(query));
|
||||
dispatch(toggleShowGlobalSearchModal());
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", {
|
||||
source: "DATASOURCE_DOCUMENTATION_CLICK",
|
||||
query,
|
||||
});
|
||||
openDoc(DocsLink.QUERY, plugin?.documentationLink, plugin?.name);
|
||||
};
|
||||
|
||||
// Added function to handle the render of the configs
|
||||
|
|
|
|||
|
|
@ -293,7 +293,8 @@ export type EventName =
|
|||
| "APP_SETTINGS_SECTION_CLICK"
|
||||
| APP_NAVIGATION_EVENT_NAMES
|
||||
| ACTION_SELECTOR_EVENT_NAMES
|
||||
| "PRETTIFY_AND_SAVE_KEYBOARD_SHORTCUT";
|
||||
| "PRETTIFY_AND_SAVE_KEYBOARD_SHORTCUT"
|
||||
| "OPEN_DOCS";
|
||||
|
||||
export type LIBRARY_EVENTS =
|
||||
| "INSTALL_LIBRARY"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user