Update slack bot listing endpoint (#4325)

* update slack bot listing endpoint

* nit
This commit is contained in:
pablonyx
2025-03-22 11:21:31 -07:00
committed by GitHub
parent fb79a9e700
commit ae3d3db3f4
3 changed files with 67 additions and 13 deletions

View File

@ -371,7 +371,9 @@ def get_all_channels_from_slack_api(
_: User | None = Depends(current_admin_user),
) -> 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.
"""
tokens = fetch_slack_bot_tokens(db_session, bot_id)
@ -386,20 +388,20 @@ def get_all_channels_from_slack_api(
current_page = 0
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:
current_page += 1
# Make API call with cursor if we have one
if next_cursor:
response = client.users_conversations(
response = client.conversations_list(
types="public_channel,private_channel",
exclude_archived=True,
cursor=next_cursor,
limit=SLACK_API_CHANNELS_PER_PAGE,
)
else:
response = client.users_conversations(
response = client.conversations_list(
types="public_channel,private_channel",
exclude_archived=True,
limit=SLACK_API_CHANNELS_PER_PAGE,

View File

@ -254,14 +254,14 @@ export function SlackChannelConfigFormFields({
onSearchTermChange={(term) => {
form.setFieldValue("channel_name", term);
}}
allowCustomValues={true}
/>
)}
</Field>
<p className="mt-2 text-sm dark:text-neutral-400 text-neutral-600">
Note: This list shows public and private channels where the
bot is a member (up to 500 channels). If you don&apos;t see a
channel, make sure the bot is added to that channel in Slack
first, or type the channel name manually.
Note: This list shows existing public and private channels (up
to 500). You can either select from the list or type any
channel name directly.
</p>
</>
)}

View File

@ -54,6 +54,7 @@ export function SearchMultiSelectDropdown({
onDelete,
onSearchTermChange,
initialSearchTerm = "",
allowCustomValues = false,
}: {
options: StringOrNumberOption[];
onSelect: (selected: StringOrNumberOption) => void;
@ -62,6 +63,7 @@ export function SearchMultiSelectDropdown({
onDelete?: (name: string) => void;
onSearchTermChange?: (term: string) => void;
initialSearchTerm?: string;
allowCustomValues?: boolean;
}) {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
@ -77,12 +79,29 @@ export function SearchMultiSelectDropdown({
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(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!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);
}
};
@ -91,7 +110,7 @@ export function SearchMultiSelectDropdown({
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
}, [allowCustomValues, searchTerm]);
useEffect(() => {
setSearchTerm(initialSearchTerm);
@ -102,17 +121,33 @@ export function SearchMultiSelectDropdown({
<div>
<input
type="text"
placeholder="Search..."
placeholder={
allowCustomValues ? "Search or enter custom value..." : "Search..."
}
value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
if (e.target.value) {
const newValue = e.target.value;
setSearchTerm(newValue);
if (onSearchTermChange) {
onSearchTermChange(newValue);
}
if (newValue) {
setIsOpen(true);
} else {
setIsOpen(false);
}
}}
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"
/>
<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 &quot;{searchTerm}&quot; as custom value
</button>
)}
{onCreate &&
searchTerm.trim() !== "" &&
!filteredOptions.some(
@ -177,7 +228,8 @@ export function SearchMultiSelectDropdown({
)}
{filteredOptions.length === 0 &&
(!onCreate || searchTerm.trim() === "") && (
((!onCreate && !allowCustomValues) ||
searchTerm.trim() === "") && (
<div className="px-4 py-2.5 text-sm text-text-500">
No matches found
</div>