mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-07-13 14:43:05 +02:00
Update slack bot listing endpoint (#4325)
* update slack bot listing endpoint * nit
This commit is contained in:
@ -371,7 +371,9 @@ def get_all_channels_from_slack_api(
|
|||||||
_: User | None = Depends(current_admin_user),
|
_: User | None = Depends(current_admin_user),
|
||||||
) -> list[SlackChannel]:
|
) -> list[SlackChannel]:
|
||||||
"""
|
"""
|
||||||
Fetches channels the bot is a member of from the Slack API.
|
Fetches all channels in the Slack workspace using the conversations_list API.
|
||||||
|
This includes both public and private channels that are visible to the app,
|
||||||
|
not just the ones the bot is a member of.
|
||||||
Handles pagination with a limit to avoid excessive API calls.
|
Handles pagination with a limit to avoid excessive API calls.
|
||||||
"""
|
"""
|
||||||
tokens = fetch_slack_bot_tokens(db_session, bot_id)
|
tokens = fetch_slack_bot_tokens(db_session, bot_id)
|
||||||
@ -386,20 +388,20 @@ def get_all_channels_from_slack_api(
|
|||||||
current_page = 0
|
current_page = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use users_conversations with limited pagination
|
# Use conversations_list to get all channels in the workspace (including ones the bot is not a member of)
|
||||||
while current_page < MAX_SLACK_PAGES:
|
while current_page < MAX_SLACK_PAGES:
|
||||||
current_page += 1
|
current_page += 1
|
||||||
|
|
||||||
# Make API call with cursor if we have one
|
# Make API call with cursor if we have one
|
||||||
if next_cursor:
|
if next_cursor:
|
||||||
response = client.users_conversations(
|
response = client.conversations_list(
|
||||||
types="public_channel,private_channel",
|
types="public_channel,private_channel",
|
||||||
exclude_archived=True,
|
exclude_archived=True,
|
||||||
cursor=next_cursor,
|
cursor=next_cursor,
|
||||||
limit=SLACK_API_CHANNELS_PER_PAGE,
|
limit=SLACK_API_CHANNELS_PER_PAGE,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = client.users_conversations(
|
response = client.conversations_list(
|
||||||
types="public_channel,private_channel",
|
types="public_channel,private_channel",
|
||||||
exclude_archived=True,
|
exclude_archived=True,
|
||||||
limit=SLACK_API_CHANNELS_PER_PAGE,
|
limit=SLACK_API_CHANNELS_PER_PAGE,
|
||||||
|
@ -254,14 +254,14 @@ export function SlackChannelConfigFormFields({
|
|||||||
onSearchTermChange={(term) => {
|
onSearchTermChange={(term) => {
|
||||||
form.setFieldValue("channel_name", term);
|
form.setFieldValue("channel_name", term);
|
||||||
}}
|
}}
|
||||||
|
allowCustomValues={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<p className="mt-2 text-sm dark:text-neutral-400 text-neutral-600">
|
<p className="mt-2 text-sm dark:text-neutral-400 text-neutral-600">
|
||||||
Note: This list shows public and private channels where the
|
Note: This list shows existing public and private channels (up
|
||||||
bot is a member (up to 500 channels). If you don't see a
|
to 500). You can either select from the list or type any
|
||||||
channel, make sure the bot is added to that channel in Slack
|
channel name directly.
|
||||||
first, or type the channel name manually.
|
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -54,6 +54,7 @@ export function SearchMultiSelectDropdown({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onSearchTermChange,
|
onSearchTermChange,
|
||||||
initialSearchTerm = "",
|
initialSearchTerm = "",
|
||||||
|
allowCustomValues = false,
|
||||||
}: {
|
}: {
|
||||||
options: StringOrNumberOption[];
|
options: StringOrNumberOption[];
|
||||||
onSelect: (selected: StringOrNumberOption) => void;
|
onSelect: (selected: StringOrNumberOption) => void;
|
||||||
@ -62,6 +63,7 @@ export function SearchMultiSelectDropdown({
|
|||||||
onDelete?: (name: string) => void;
|
onDelete?: (name: string) => void;
|
||||||
onSearchTermChange?: (term: string) => void;
|
onSearchTermChange?: (term: string) => void;
|
||||||
initialSearchTerm?: string;
|
initialSearchTerm?: string;
|
||||||
|
allowCustomValues?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
|
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
|
||||||
@ -77,12 +79,29 @@ export function SearchMultiSelectDropdown({
|
|||||||
option.name.toLowerCase().includes(searchTerm.toLowerCase())
|
option.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle selecting a custom value not in the options list
|
||||||
|
const handleCustomValueSelect = () => {
|
||||||
|
if (allowCustomValues && searchTerm.trim() !== "") {
|
||||||
|
const customOption: StringOrNumberOption = {
|
||||||
|
name: searchTerm,
|
||||||
|
value: searchTerm,
|
||||||
|
};
|
||||||
|
onSelect(customOption);
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
dropdownRef.current &&
|
dropdownRef.current &&
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
) {
|
) {
|
||||||
|
// If allowCustomValues is enabled and there's text in the search field,
|
||||||
|
// treat clicking outside as selecting the custom value
|
||||||
|
if (allowCustomValues && searchTerm.trim() !== "") {
|
||||||
|
handleCustomValueSelect();
|
||||||
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -91,7 +110,7 @@ export function SearchMultiSelectDropdown({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [allowCustomValues, searchTerm]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchTerm(initialSearchTerm);
|
setSearchTerm(initialSearchTerm);
|
||||||
@ -102,17 +121,33 @@ export function SearchMultiSelectDropdown({
|
|||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder={
|
||||||
|
allowCustomValues ? "Search or enter custom value..." : "Search..."
|
||||||
|
}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchTerm(e.target.value);
|
const newValue = e.target.value;
|
||||||
if (e.target.value) {
|
setSearchTerm(newValue);
|
||||||
|
if (onSearchTermChange) {
|
||||||
|
onSearchTermChange(newValue);
|
||||||
|
}
|
||||||
|
if (newValue) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
} else {
|
} else {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => setIsOpen(true)}
|
onFocus={() => setIsOpen(true)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (
|
||||||
|
e.key === "Enter" &&
|
||||||
|
allowCustomValues &&
|
||||||
|
searchTerm.trim() !== ""
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCustomValueSelect();
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-white dark:bg-transparent text-text-800 border border-background-300 rounded-md shadow-sm"
|
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-white dark:bg-transparent text-text-800 border border-background-300 rounded-md shadow-sm"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@ -153,6 +188,22 @@ export function SearchMultiSelectDropdown({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allowCustomValues &&
|
||||||
|
searchTerm.trim() !== "" &&
|
||||||
|
!filteredOptions.some(
|
||||||
|
(option) =>
|
||||||
|
option.name.toLowerCase() === searchTerm.toLowerCase()
|
||||||
|
) && (
|
||||||
|
<button
|
||||||
|
className="w-full text-left flex items-center px-4 py-2 text-sm text-text-800 hover:bg-background-100"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={handleCustomValueSelect}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4 mr-2 text-text-600" />
|
||||||
|
Use "{searchTerm}" as custom value
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{onCreate &&
|
{onCreate &&
|
||||||
searchTerm.trim() !== "" &&
|
searchTerm.trim() !== "" &&
|
||||||
!filteredOptions.some(
|
!filteredOptions.some(
|
||||||
@ -177,7 +228,8 @@ export function SearchMultiSelectDropdown({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredOptions.length === 0 &&
|
{filteredOptions.length === 0 &&
|
||||||
(!onCreate || searchTerm.trim() === "") && (
|
((!onCreate && !allowCustomValues) ||
|
||||||
|
searchTerm.trim() === "") && (
|
||||||
<div className="px-4 py-2.5 text-sm text-text-500">
|
<div className="px-4 py-2.5 text-sm text-text-500">
|
||||||
No matches found
|
No matches found
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user