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), _: 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,

View File

@ -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&apos;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>
</> </>
)} )}

View File

@ -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 &quot;{searchTerm}&quot; 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>