mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-08-03 13:43:18 +02:00
Added ClickUp Connector (#1521)
* Added connector for clickup * Fixed mypy issues * Fallback to description if markdown is not available * Added extra information in metadata, and support to index comments * Fixes for fields parsing * updated fetcher to errorHandlingFetcher --------- Co-authored-by: hagen-danswer <hagen@danswer.ai>
This commit is contained in:
@@ -98,6 +98,7 @@ class DocumentSource(str, Enum):
|
|||||||
TEAMS = "teams"
|
TEAMS = "teams"
|
||||||
DISCOURSE = "discourse"
|
DISCOURSE = "discourse"
|
||||||
AXERO = "axero"
|
AXERO = "axero"
|
||||||
|
CLICKUP = "clickup"
|
||||||
MEDIAWIKI = "mediawiki"
|
MEDIAWIKI = "mediawiki"
|
||||||
WIKIPEDIA = "wikipedia"
|
WIKIPEDIA = "wikipedia"
|
||||||
|
|
||||||
|
0
backend/danswer/connectors/clickup/__init__.py
Normal file
0
backend/danswer/connectors/clickup/__init__.py
Normal file
216
backend/danswer/connectors/clickup/connector.py
Normal file
216
backend/danswer/connectors/clickup/connector.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from datetime import timezone
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from danswer.configs.app_configs import INDEX_BATCH_SIZE
|
||||||
|
from danswer.configs.constants import DocumentSource
|
||||||
|
from danswer.connectors.cross_connector_utils.rate_limit_wrapper import (
|
||||||
|
rate_limit_builder,
|
||||||
|
)
|
||||||
|
from danswer.connectors.cross_connector_utils.retry_wrapper import retry_builder
|
||||||
|
from danswer.connectors.interfaces import GenerateDocumentsOutput
|
||||||
|
from danswer.connectors.interfaces import LoadConnector
|
||||||
|
from danswer.connectors.interfaces import PollConnector
|
||||||
|
from danswer.connectors.interfaces import SecondsSinceUnixEpoch
|
||||||
|
from danswer.connectors.models import BasicExpertInfo
|
||||||
|
from danswer.connectors.models import ConnectorMissingCredentialError
|
||||||
|
from danswer.connectors.models import Document
|
||||||
|
from danswer.connectors.models import Section
|
||||||
|
|
||||||
|
|
||||||
|
CLICKUP_API_BASE_URL = "https://api.clickup.com/api/v2"
|
||||||
|
|
||||||
|
|
||||||
|
class ClickupConnector(LoadConnector, PollConnector):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
batch_size: int = INDEX_BATCH_SIZE,
|
||||||
|
api_token: str | None = None,
|
||||||
|
team_id: str | None = None,
|
||||||
|
connector_type: str | None = None,
|
||||||
|
connector_ids: list[str] | None = None,
|
||||||
|
retrieve_task_comments: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.batch_size = batch_size
|
||||||
|
self.api_token = api_token
|
||||||
|
self.team_id = team_id
|
||||||
|
self.connector_type = connector_type if connector_type else "workspace"
|
||||||
|
self.connector_ids = connector_ids
|
||||||
|
self.retrieve_task_comments = retrieve_task_comments
|
||||||
|
|
||||||
|
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
self.api_token = credentials["clickup_api_token"]
|
||||||
|
self.team_id = credentials["clickup_team_id"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@retry_builder()
|
||||||
|
@rate_limit_builder(max_calls=100, period=60)
|
||||||
|
def _make_request(self, endpoint: str, params: Optional[dict] = None) -> Any:
|
||||||
|
if not self.api_token:
|
||||||
|
raise ConnectorMissingCredentialError("Clickup")
|
||||||
|
|
||||||
|
headers = {"Authorization": self.api_token}
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
f"{CLICKUP_API_BASE_URL}/{endpoint}", headers=headers, params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def _get_task_comments(self, task_id: str) -> list[Section]:
|
||||||
|
url_endpoint = f"/task/{task_id}/comment"
|
||||||
|
response = self._make_request(url_endpoint)
|
||||||
|
comments = [
|
||||||
|
Section(
|
||||||
|
link=f'https://app.clickup.com/t/{task_id}?comment={comment_dict["id"]}',
|
||||||
|
text=comment_dict["comment_text"],
|
||||||
|
)
|
||||||
|
for comment_dict in response["comments"]
|
||||||
|
]
|
||||||
|
|
||||||
|
return comments
|
||||||
|
|
||||||
|
def _get_all_tasks_filtered(
|
||||||
|
self,
|
||||||
|
start: int | None = None,
|
||||||
|
end: int | None = None,
|
||||||
|
) -> GenerateDocumentsOutput:
|
||||||
|
doc_batch: list[Document] = []
|
||||||
|
page: int = 0
|
||||||
|
params = {
|
||||||
|
"include_markdown_description": "true",
|
||||||
|
"include_closed": "true",
|
||||||
|
"page": page,
|
||||||
|
}
|
||||||
|
|
||||||
|
if start is not None:
|
||||||
|
params["date_updated_gt"] = start
|
||||||
|
if end is not None:
|
||||||
|
params["date_updated_lt"] = end
|
||||||
|
|
||||||
|
if self.connector_type == "list":
|
||||||
|
params["list_ids[]"] = self.connector_ids
|
||||||
|
elif self.connector_type == "folder":
|
||||||
|
params["project_ids[]"] = self.connector_ids
|
||||||
|
elif self.connector_type == "space":
|
||||||
|
params["space_ids[]"] = self.connector_ids
|
||||||
|
|
||||||
|
url_endpoint = f"/team/{self.team_id}/task"
|
||||||
|
|
||||||
|
while True:
|
||||||
|
response = self._make_request(url_endpoint, params)
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
params["page"] = page
|
||||||
|
|
||||||
|
for task in response["tasks"]:
|
||||||
|
document = Document(
|
||||||
|
id=task["id"],
|
||||||
|
source=DocumentSource.CLICKUP,
|
||||||
|
semantic_identifier=task["name"],
|
||||||
|
doc_updated_at=(
|
||||||
|
datetime.fromtimestamp(
|
||||||
|
round(float(task["date_updated"]) / 1000, 3)
|
||||||
|
).replace(tzinfo=timezone.utc)
|
||||||
|
),
|
||||||
|
primary_owners=[
|
||||||
|
BasicExpertInfo(
|
||||||
|
display_name=task["creator"]["username"],
|
||||||
|
email=task["creator"]["email"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
secondary_owners=[
|
||||||
|
BasicExpertInfo(
|
||||||
|
display_name=assignee["username"],
|
||||||
|
email=assignee["email"],
|
||||||
|
)
|
||||||
|
for assignee in task["assignees"]
|
||||||
|
],
|
||||||
|
title=task["name"],
|
||||||
|
sections=[
|
||||||
|
Section(
|
||||||
|
link=task["url"],
|
||||||
|
text=(
|
||||||
|
task["markdown_description"]
|
||||||
|
if "markdown_description" in task
|
||||||
|
else task["description"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
metadata={
|
||||||
|
"id": task["id"],
|
||||||
|
"status": task["status"]["status"],
|
||||||
|
"list": task["list"]["name"],
|
||||||
|
"project": task["project"]["name"],
|
||||||
|
"folder": task["folder"]["name"],
|
||||||
|
"space_id": task["space"]["id"],
|
||||||
|
"tags": [tag["name"] for tag in task["tags"]],
|
||||||
|
"priority": (
|
||||||
|
task["priority"]["priority"]
|
||||||
|
if "priority" in task and task["priority"] is not None
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
extra_fields = [
|
||||||
|
"date_created",
|
||||||
|
"date_updated",
|
||||||
|
"date_closed",
|
||||||
|
"date_done",
|
||||||
|
"due_date",
|
||||||
|
]
|
||||||
|
for extra_field in extra_fields:
|
||||||
|
if extra_field in task and task[extra_field] is not None:
|
||||||
|
document.metadata[extra_field] = task[extra_field]
|
||||||
|
|
||||||
|
if self.retrieve_task_comments:
|
||||||
|
document.sections.extend(self._get_task_comments(task["id"]))
|
||||||
|
|
||||||
|
doc_batch.append(document)
|
||||||
|
|
||||||
|
if len(doc_batch) >= self.batch_size:
|
||||||
|
yield doc_batch
|
||||||
|
doc_batch = []
|
||||||
|
|
||||||
|
if response.get("last_page") is True or len(response["tasks"]) < 100:
|
||||||
|
break
|
||||||
|
|
||||||
|
if doc_batch:
|
||||||
|
yield doc_batch
|
||||||
|
|
||||||
|
def load_from_state(self) -> GenerateDocumentsOutput:
|
||||||
|
if self.api_token is None:
|
||||||
|
raise ConnectorMissingCredentialError("Clickup")
|
||||||
|
|
||||||
|
return self._get_all_tasks_filtered(None, None)
|
||||||
|
|
||||||
|
def poll_source(
|
||||||
|
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||||
|
) -> GenerateDocumentsOutput:
|
||||||
|
if self.api_token is None:
|
||||||
|
raise ConnectorMissingCredentialError("Clickup")
|
||||||
|
|
||||||
|
return self._get_all_tasks_filtered(int(start * 1000), int(end * 1000))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import os
|
||||||
|
|
||||||
|
clickup_connector = ClickupConnector()
|
||||||
|
|
||||||
|
clickup_connector.load_credentials(
|
||||||
|
{
|
||||||
|
"clickup_api_token": os.environ["clickup_api_token"],
|
||||||
|
"clickup_team_id": os.environ["clickup_team_id"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
latest_docs = clickup_connector.load_from_state()
|
||||||
|
|
||||||
|
for doc in latest_docs:
|
||||||
|
print(doc)
|
@@ -4,6 +4,7 @@ from typing import Type
|
|||||||
from danswer.configs.constants import DocumentSource
|
from danswer.configs.constants import DocumentSource
|
||||||
from danswer.connectors.axero.connector import AxeroConnector
|
from danswer.connectors.axero.connector import AxeroConnector
|
||||||
from danswer.connectors.bookstack.connector import BookstackConnector
|
from danswer.connectors.bookstack.connector import BookstackConnector
|
||||||
|
from danswer.connectors.clickup.connector import ClickupConnector
|
||||||
from danswer.connectors.confluence.connector import ConfluenceConnector
|
from danswer.connectors.confluence.connector import ConfluenceConnector
|
||||||
from danswer.connectors.danswer_jira.connector import JiraConnector
|
from danswer.connectors.danswer_jira.connector import JiraConnector
|
||||||
from danswer.connectors.discourse.connector import DiscourseConnector
|
from danswer.connectors.discourse.connector import DiscourseConnector
|
||||||
@@ -80,6 +81,7 @@ def identify_connector_class(
|
|||||||
DocumentSource.TEAMS: TeamsConnector,
|
DocumentSource.TEAMS: TeamsConnector,
|
||||||
DocumentSource.DISCOURSE: DiscourseConnector,
|
DocumentSource.DISCOURSE: DiscourseConnector,
|
||||||
DocumentSource.AXERO: AxeroConnector,
|
DocumentSource.AXERO: AxeroConnector,
|
||||||
|
DocumentSource.CLICKUP: ClickupConnector,
|
||||||
DocumentSource.MEDIAWIKI: MediaWikiConnector,
|
DocumentSource.MEDIAWIKI: MediaWikiConnector,
|
||||||
DocumentSource.WIKIPEDIA: WikipediaConnector,
|
DocumentSource.WIKIPEDIA: WikipediaConnector,
|
||||||
}
|
}
|
||||||
|
1
web/public/Clickup.svg
Normal file
1
web/public/Clickup.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="130" height="155" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="0%" y1="68.01%" y2="68.01%" id="a"><stop stop-color="#8930FD" offset="0%"/><stop stop-color="#49CCF9" offset="100%"/></linearGradient><linearGradient x1="0%" y1="68.01%" y2="68.01%" id="b"><stop stop-color="#FF02F0" offset="0%"/><stop stop-color="#FFC800" offset="100%"/></linearGradient></defs><g fill-rule="nonzero" fill="none"><path d="M.4 119.12l23.81-18.24C36.86 117.39 50.3 125 65.26 125c14.88 0 27.94-7.52 40.02-23.9l24.15 17.8C112 142.52 90.34 155 65.26 155c-25 0-46.87-12.4-64.86-35.88z" fill="url(#a)"/><path fill="url(#b)" d="M65.18 39.84L22.8 76.36 3.21 53.64 65.27.16l61.57 53.52-19.68 22.64z"/></g></svg>
|
After Width: | Height: | Size: 709 B |
343
web/src/app/admin/connectors/clickup/page.tsx
Normal file
343
web/src/app/admin/connectors/clickup/page.tsx
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { TrashIcon, ClickupIcon } from "@/components/icons/icons";
|
||||||
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
|
import {
|
||||||
|
ClickupConfig,
|
||||||
|
ClickupCredentialJson,
|
||||||
|
ConnectorIndexingStatus,
|
||||||
|
Credential,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||||
|
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||||
|
import {
|
||||||
|
BooleanFormField,
|
||||||
|
SelectorFormField,
|
||||||
|
TextFormField,
|
||||||
|
TextArrayFieldBuilder,
|
||||||
|
} from "@/components/admin/connectors/Field";
|
||||||
|
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||||
|
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { usePublicCredentials } from "@/lib/hooks";
|
||||||
|
import { Title, Text, Card, Divider } from "@tremor/react";
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
|
||||||
|
const MainSection = () => {
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
const {
|
||||||
|
data: connectorIndexingStatuses,
|
||||||
|
isLoading: isConnectorIndexingStatusesLoading,
|
||||||
|
error: isConnectorIndexingStatusesError,
|
||||||
|
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||||
|
"/api/manage/admin/connector/indexing-status",
|
||||||
|
errorHandlingFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: credentialsData,
|
||||||
|
isLoading: isCredentialsLoading,
|
||||||
|
error: isCredentialsError,
|
||||||
|
refreshCredentials,
|
||||||
|
} = usePublicCredentials();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
|
||||||
|
(!credentialsData && isCredentialsLoading)
|
||||||
|
) {
|
||||||
|
return <LoadingAnimation text="Loading" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) {
|
||||||
|
return <div>Failed to load connectors</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCredentialsError || !credentialsData) {
|
||||||
|
return <div>Failed to load credentials</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickupConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||||
|
ClickupConfig,
|
||||||
|
ClickupCredentialJson
|
||||||
|
>[] = connectorIndexingStatuses.filter(
|
||||||
|
(connectorIndexingStatus) =>
|
||||||
|
connectorIndexingStatus.connector.source === "clickup"
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickupCredential: Credential<ClickupCredentialJson> | undefined =
|
||||||
|
credentialsData.find(
|
||||||
|
(credential) => credential.credential_json?.clickup_api_token
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{popup}
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 1: Provide Credentials
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{clickupCredential ? (
|
||||||
|
<>
|
||||||
|
<div className="flex mb-1 text-sm">
|
||||||
|
<Text className="my-auto">Existing Clickup API Token: </Text>
|
||||||
|
<Text className="ml-1 italic my-auto">
|
||||||
|
{clickupCredential.credential_json.clickup_api_token}
|
||||||
|
</Text>
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:bg-hover rounded p-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (clickupConnectorIndexingStatuses.length > 0) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Must delete all connectors before deleting credentials",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await adminDeleteCredential(clickupCredential.id);
|
||||||
|
refreshCredentials();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text className="mb-4">
|
||||||
|
To use the Clickup connector, you must first provide the API token
|
||||||
|
and Team ID corresponding to your Clickup setup. See setup guide{" "}
|
||||||
|
<a
|
||||||
|
className="text-link"
|
||||||
|
href="https://docs.danswer.dev/connectors/clickup"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>{" "}
|
||||||
|
for more detail.
|
||||||
|
</Text>
|
||||||
|
<Card className="mt-2">
|
||||||
|
<CredentialForm<ClickupCredentialJson>
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<TextFormField
|
||||||
|
name="clickup_api_token"
|
||||||
|
label="Clickup API Token:"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<TextFormField name="clickup_team_id" label="Team ID:" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
clickup_api_token: Yup.string().required(
|
||||||
|
"Please enter your Clickup API token"
|
||||||
|
),
|
||||||
|
clickup_team_id: Yup.string().required(
|
||||||
|
"Please enter your Team ID"
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
clickup_api_token: "",
|
||||||
|
clickup_team_id: "",
|
||||||
|
}}
|
||||||
|
onSubmit={(isSuccess) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
refreshCredentials();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 2: Do you want to search particular space(s), folder(s), list(s),
|
||||||
|
or entire workspace?
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{clickupConnectorIndexingStatuses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text className="mb-2">
|
||||||
|
We index the latest articles from either the entire workspace, or
|
||||||
|
specified space(s), folder(s), list(s) listed below regularly.
|
||||||
|
</Text>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ConnectorsTable<ClickupConfig, ClickupCredentialJson>
|
||||||
|
connectorIndexingStatuses={clickupConnectorIndexingStatuses}
|
||||||
|
liveCredential={clickupCredential}
|
||||||
|
getCredential={(credential) =>
|
||||||
|
credential.credential_json.clickup_api_token
|
||||||
|
}
|
||||||
|
specialColumns={[
|
||||||
|
{
|
||||||
|
header: "Connector Type",
|
||||||
|
key: "connector_type",
|
||||||
|
getValue: (ccPairStatus) =>
|
||||||
|
ccPairStatus.connector.connector_specific_config
|
||||||
|
.connector_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "ID(s)",
|
||||||
|
key: "connector_ids",
|
||||||
|
getValue: (ccPairStatus) =>
|
||||||
|
ccPairStatus.connector.connector_specific_config
|
||||||
|
.connector_ids &&
|
||||||
|
ccPairStatus.connector.connector_specific_config
|
||||||
|
.connector_ids.length > 0
|
||||||
|
? ccPairStatus.connector.connector_specific_config.connector_ids.join(
|
||||||
|
", "
|
||||||
|
)
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Retrieve Task Comments?",
|
||||||
|
key: "retrieve_task_comments",
|
||||||
|
getValue: (ccPairStatus) =>
|
||||||
|
ccPairStatus.connector.connector_specific_config
|
||||||
|
.retrieve_task_comments
|
||||||
|
? "Yes"
|
||||||
|
: "No",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onUpdate={() =>
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status")
|
||||||
|
}
|
||||||
|
onCredentialLink={async (connectorId) => {
|
||||||
|
if (clickupCredential) {
|
||||||
|
await linkCredential(connectorId, clickupCredential.id);
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clickupCredential ? (
|
||||||
|
<Card className="mt-4">
|
||||||
|
<h2 className="font-bold mb-3">Connect to a New Workspace</h2>
|
||||||
|
<ConnectorForm<ClickupConfig>
|
||||||
|
nameBuilder={(values) =>
|
||||||
|
values.connector_ids
|
||||||
|
? `ClickupConnector-${
|
||||||
|
values.connector_type
|
||||||
|
}-${values.connector_ids.join("_")}`
|
||||||
|
: `ClickupConnector-${values.connector_type}`
|
||||||
|
}
|
||||||
|
source="clickup"
|
||||||
|
inputType="poll"
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<SelectorFormField
|
||||||
|
name="connector_type"
|
||||||
|
label="Connector Type:"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
name: "Entire Workspace",
|
||||||
|
value: "workspace",
|
||||||
|
description:
|
||||||
|
"Recursively index all tasks from the entire workspace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Space(s)",
|
||||||
|
value: "space",
|
||||||
|
description:
|
||||||
|
"Index tasks only from the specified space id(s).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Folder(s)",
|
||||||
|
value: "folder",
|
||||||
|
description:
|
||||||
|
"Index tasks only from the specified folder id(s).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "List(s)",
|
||||||
|
value: "list",
|
||||||
|
description:
|
||||||
|
"Index tasks only from the specified list id(s).",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
formBodyBuilder={(values) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
{TextArrayFieldBuilder({
|
||||||
|
name: "connector_ids",
|
||||||
|
label: "ID(s):",
|
||||||
|
subtext: "Specify 0 or more id(s) to index from.",
|
||||||
|
})(values)}
|
||||||
|
<BooleanFormField
|
||||||
|
name="retrieve_task_comments"
|
||||||
|
label="Retrieve Task Comments?"
|
||||||
|
subtext={
|
||||||
|
"If checked, then all the comments for each task will also be retrieved and indexed."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
connector_type: Yup.string()
|
||||||
|
.oneOf(["workspace", "space", "folder", "list"])
|
||||||
|
.required("Please select the connector_type to index"),
|
||||||
|
connector_ids: Yup.array()
|
||||||
|
.of(Yup.string().required("ID(s) must be strings"))
|
||||||
|
.test(
|
||||||
|
"idsRequired",
|
||||||
|
"At least 1 ID is required if space, folder or list is selected",
|
||||||
|
function (value) {
|
||||||
|
if (this.parent.connector_type === "workspace") return true;
|
||||||
|
else if (value !== undefined && value.length > 0)
|
||||||
|
return true;
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message: `Add at least one ${this.parent.connector_type} ID`,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
retrieve_task_comments: Yup.boolean().required(),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
connector_type: "workspace",
|
||||||
|
connector_ids: [],
|
||||||
|
retrieve_task_comments: true,
|
||||||
|
}}
|
||||||
|
refreshFreq={10 * 60} // 10 minutes
|
||||||
|
credentialId={clickupCredential.id}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
Please provide your Clickup API token and Team ID in Step 1 first!
|
||||||
|
Once done with that, you can then specify whether you want to make the
|
||||||
|
entire workspace, or specified space(s), folder(s), list(s)
|
||||||
|
searchable.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto container">
|
||||||
|
<div className="mb-4">
|
||||||
|
<HealthCheckBanner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AdminPageTitle icon={<ClickupIcon size={32} />} title="Clickup" />
|
||||||
|
|
||||||
|
<MainSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -57,6 +57,7 @@ import teamsIcon from "../../../public/Teams.png";
|
|||||||
import mediawikiIcon from "../../../public/MediaWiki.svg";
|
import mediawikiIcon from "../../../public/MediaWiki.svg";
|
||||||
import wikipediaIcon from "../../../public/Wikipedia.svg";
|
import wikipediaIcon from "../../../public/Wikipedia.svg";
|
||||||
import discourseIcon from "../../../public/Discourse.png";
|
import discourseIcon from "../../../public/Discourse.png";
|
||||||
|
import clickupIcon from "../../../public/Clickup.svg";
|
||||||
import { FaRobot } from "react-icons/fa";
|
import { FaRobot } from "react-icons/fa";
|
||||||
|
|
||||||
interface IconProps {
|
interface IconProps {
|
||||||
@@ -654,6 +655,20 @@ export const AxeroIcon = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ClickupIcon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
|
||||||
|
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
|
||||||
|
>
|
||||||
|
<Image src={clickupIcon} alt="Logo" width="96" height="96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const MediaWikiIcon = ({
|
export const MediaWikiIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AxeroIcon,
|
AxeroIcon,
|
||||||
BookstackIcon,
|
BookstackIcon,
|
||||||
|
ClickupIcon,
|
||||||
ConfluenceIcon,
|
ConfluenceIcon,
|
||||||
DiscourseIcon,
|
DiscourseIcon,
|
||||||
Document360Icon,
|
Document360Icon,
|
||||||
@@ -195,6 +196,11 @@ const SOURCE_METADATA_MAP: SourceMap = {
|
|||||||
displayName: "Request Tracker",
|
displayName: "Request Tracker",
|
||||||
category: SourceCategory.AppConnection,
|
category: SourceCategory.AppConnection,
|
||||||
},
|
},
|
||||||
|
clickup: {
|
||||||
|
icon: ClickupIcon,
|
||||||
|
displayName: "Clickup",
|
||||||
|
category: SourceCategory.AppConnection,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function fillSourceMetadata(
|
function fillSourceMetadata(
|
||||||
|
@@ -48,6 +48,8 @@ export type ValidSources =
|
|||||||
| "zendesk"
|
| "zendesk"
|
||||||
| "discourse"
|
| "discourse"
|
||||||
| "axero"
|
| "axero"
|
||||||
|
| "clickup"
|
||||||
|
| "axero"
|
||||||
| "wikipedia"
|
| "wikipedia"
|
||||||
| "mediawiki";
|
| "mediawiki";
|
||||||
|
|
||||||
@@ -189,6 +191,12 @@ export interface Document360Config {
|
|||||||
categories?: string[];
|
categories?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClickupConfig {
|
||||||
|
connector_type: "list" | "folder" | "space" | "workspace";
|
||||||
|
connector_ids?: string[];
|
||||||
|
retrieve_task_comments: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GoogleSitesConfig {
|
export interface GoogleSitesConfig {
|
||||||
zip_path: string;
|
zip_path: string;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
@@ -364,6 +372,11 @@ export interface Document360CredentialJson {
|
|||||||
document360_api_token: string;
|
document360_api_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClickupCredentialJson {
|
||||||
|
clickup_api_token: string;
|
||||||
|
clickup_team_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ZendeskCredentialJson {
|
export interface ZendeskCredentialJson {
|
||||||
zendesk_subdomain: string;
|
zendesk_subdomain: string;
|
||||||
zendesk_email: string;
|
zendesk_email: string;
|
||||||
|
Reference in New Issue
Block a user