mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
teams improvements if there are a large number of connected teams (#5694)
* teams improvements if there are a large number of connected teams / channels * move sync to backend * formatting * update ee ref
This commit is contained in:
@@ -1 +1 @@
|
||||
868ccad87afb804fe22818ecd3d5a091199bcdbf
|
||||
19b5681d1985ee5214f768e3b818faf6ca7d20b2
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import Select from '$lib/components/apps/svelte-select/lib/Select.svelte'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
|
||||
interface ChannelItem {
|
||||
channel_id: string
|
||||
channel_name: string
|
||||
}
|
||||
|
||||
export let disabled = false
|
||||
export let placeholder = 'Select channel'
|
||||
export let selectedChannel: ChannelItem | undefined = undefined
|
||||
export let containerClass = 'w-64'
|
||||
export let minWidth = '160px'
|
||||
export let channels: ChannelItem[] = []
|
||||
|
||||
let darkMode: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: ChannelItem
|
||||
}>()
|
||||
|
||||
function onThemeChange() {
|
||||
darkMode = document.documentElement.classList.contains('dark')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onThemeChange()
|
||||
})
|
||||
|
||||
function handleChannelSelect(event) {
|
||||
selectedChannel = event.detail
|
||||
if (selectedChannel) {
|
||||
dispatch('change', selectedChannel)
|
||||
}
|
||||
}
|
||||
|
||||
function filterChannels(filterText: string | unknown) {
|
||||
if (!channels) return channels
|
||||
|
||||
const searchText = typeof filterText === 'string' ? filterText : ''
|
||||
|
||||
const filtered = searchText
|
||||
? channels.filter((channel) =>
|
||||
channel.channel_name.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
: channels
|
||||
|
||||
return filtered
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={containerClass}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-grow" style="min-width: {minWidth};">
|
||||
<Select
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={'border-color: lightgray; min-width: ' +
|
||||
minWidth +
|
||||
';' +
|
||||
(darkMode
|
||||
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
|
||||
: SELECT_INPUT_DEFAULT_STYLE.containerStyles)}
|
||||
itemId="channel_id"
|
||||
label="channel_name"
|
||||
items={channels || []}
|
||||
on:change={handleChannelSelect}
|
||||
{placeholder}
|
||||
searchable={true}
|
||||
disabled={disabled || channels.length === 0}
|
||||
on:input={async (e) => {
|
||||
const filterText = e.detail
|
||||
return filterChannels(filterText)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if channels.length === 0 && !disabled}
|
||||
<div class="text-xs text-tertiary mt-1"> No channels available </div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DarkModeObserver on:change={onThemeChange} />
|
||||
@@ -1,15 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { Slack, Code2, RefreshCcw } from 'lucide-svelte'
|
||||
import { Slack, Code2 } from 'lucide-svelte'
|
||||
import MsTeamsIcon from '$lib/components/icons/MSTeamsIcon.svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import { hubBaseUrlStore, workspaceStore, enterpriseLicense } from '$lib/stores'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import type { ListAvailableTeamsIdsResponse } from '$lib/gen/types.gen'
|
||||
export let platform: 'slack' | 'teams'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import TeamSelector from './TeamSelector.svelte'
|
||||
|
||||
interface TeamItem {
|
||||
team_id: string
|
||||
team_name: string
|
||||
}
|
||||
|
||||
export let platform: 'slack' | 'teams'
|
||||
export let teamName: string | undefined
|
||||
export let display_name: string | undefined
|
||||
export let scriptPath: string
|
||||
@@ -23,34 +29,25 @@
|
||||
export let onLoadSettings: () => void
|
||||
export let itemKind: 'flow' | 'script' = 'script'
|
||||
|
||||
let isFetching = false
|
||||
|
||||
let teams: ListAvailableTeamsIdsResponse = []
|
||||
let selected_teams_team: string | undefined = undefined
|
||||
|
||||
async function loadTeams() {
|
||||
isFetching = true
|
||||
selected_teams_team = undefined
|
||||
teams = (await WorkspaceService.listAvailableTeamsIds({ workspace: $workspaceStore! })) ?? []
|
||||
isFetching = false
|
||||
}
|
||||
|
||||
$: workspaceStore && platform && $enterpriseLicense && loadTeams()
|
||||
let selectedTeam: TeamItem | undefined = undefined
|
||||
|
||||
async function connectTeams() {
|
||||
const selectedTeam = teams.find((team) => team.team_id === selected_teams_team)
|
||||
const selectedTeamName = selectedTeam ? selectedTeam.team_name : undefined
|
||||
if (!selectedTeam) return
|
||||
|
||||
await WorkspaceService.connectTeams({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
team_id: selected_teams_team,
|
||||
team_name: selectedTeamName
|
||||
}
|
||||
})
|
||||
sendUserToast('Connected to Teams to Workspace')
|
||||
onLoadSettings()
|
||||
loadTeams()
|
||||
try {
|
||||
await WorkspaceService.connectTeams({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
team_id: selectedTeam.team_id,
|
||||
team_name: selectedTeam.team_name
|
||||
}
|
||||
})
|
||||
sendUserToast('Connected to Teams successfully')
|
||||
onLoadSettings()
|
||||
} catch (error) {
|
||||
sendUserToast('Failed to connect to Teams', true)
|
||||
console.error('Error connecting to Teams:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -92,49 +89,35 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-2">
|
||||
{#if platform === 'teams'}
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={connectTeams}
|
||||
startIcon={{ icon: MsTeamsIcon }}
|
||||
disabled={!selected_teams_team || !enterpriseLicense}
|
||||
>
|
||||
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
|
||||
{$enterpriseLicense ? '' : '(EE only)'}
|
||||
</Button>
|
||||
{#if $enterpriseLicense}
|
||||
<div class="w-64 flex flex-row gap-2">
|
||||
<select bind:value={selected_teams_team}>
|
||||
{#if !isFetching}
|
||||
{#if teams.length === 0}
|
||||
<option value="" disabled selected>No unassigned teams found</option>
|
||||
{:else}
|
||||
<option value="" disabled selected>Select team</option>
|
||||
{#each teams as team}
|
||||
<option value={team.team_id}>
|
||||
{team.team_name}
|
||||
</option>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<option value="" disabled selected>Loading...</option>
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
<div class="pt-1">
|
||||
<button on:click={loadTeams} class="flex items-center gap-1 mt-2">
|
||||
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
{#if platform === 'teams'}
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={connectTeams}
|
||||
startIcon={{ icon: MsTeamsIcon }}
|
||||
disabled={!selectedTeam || !$enterpriseLicense}
|
||||
>
|
||||
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
|
||||
{$enterpriseLicense ? '' : '(EE only)'}
|
||||
</Button>
|
||||
|
||||
{#if $enterpriseLicense}
|
||||
<TeamSelector
|
||||
bind:selectedTeam
|
||||
minWidth="180px"
|
||||
disabled={!$enterpriseLicense}
|
||||
on:error={(e) => sendUserToast('Failed to load teams: ' + e.detail.message, true)}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button size="xs" color="dark" href={connectHref} startIcon={{ icon: Slack }}>
|
||||
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
|
||||
</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button size="xs" color="dark" href={connectHref} startIcon={{ icon: Slack }}>
|
||||
Connect to {platform.charAt(0).toUpperCase() + platform.slice(1)}
|
||||
</Button>
|
||||
{/if}
|
||||
<Badge color="red">Not connected</Badge>
|
||||
<Badge color="red">Not connected</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ChannelSelector from '$lib/components/ChannelSelector.svelte'
|
||||
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import { base } from '$lib/base'
|
||||
@@ -439,34 +440,40 @@
|
||||
/>
|
||||
</span>
|
||||
{#if workspaceConnectedToTeams}
|
||||
<div class="w-2/3 flex flex-row items-center gap-2">
|
||||
<div class="pt-1 flex-shrink-0">
|
||||
<MsTeamsIcon height="24px" width="24px" />
|
||||
<div class="w-2/3 flex flex-col gap-2">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="pt-1 flex-shrink-0">
|
||||
<MsTeamsIcon height="24px" width="24px" />
|
||||
</div>
|
||||
<p class="text-sm">Teams Channel</p>
|
||||
</div>
|
||||
<p class="text-sm">Teams Channel</p>
|
||||
<div class="flex-grow">
|
||||
<select class="w-full" bind:value={handlerExtraArgs['channel']}>
|
||||
{#if teams_channels.length === 0}
|
||||
<option value="" disabled selected>Bot not connected to any channel</option>
|
||||
{:else}
|
||||
<option value="" disabled selected>Select Teams channel</option>
|
||||
{#each teams_channels as channel}
|
||||
<option value={channel.channel_id}>
|
||||
{channel.channel_name}
|
||||
</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<button on:click={loadTeamsResources} class="flex items-center gap-1 mt-2">
|
||||
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
|
||||
<div class="flex flex-row gap-2 items-start">
|
||||
<ChannelSelector
|
||||
containerClass="flex-grow"
|
||||
minWidth="200px"
|
||||
placeholder="Select Teams channel"
|
||||
channels={teams_channels as any[]}
|
||||
on:change={(e) => (handlerExtraArgs['channel'] = e.detail.channel_id)}
|
||||
selectedChannel={handlerExtraArgs['channel']
|
||||
? (teams_channels.find((ch) => ch.channel_id === handlerExtraArgs['channel']) as any)
|
||||
: undefined}
|
||||
/>
|
||||
<div class="flex-shrink-0">
|
||||
<button
|
||||
on:click={loadTeamsResources}
|
||||
class="flex items-center gap-1 p-1.5 rounded hover:bg-surface-hover focus:bg-surface-hover"
|
||||
>
|
||||
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 pb-4">
|
||||
<p class="text-sm">
|
||||
This workspace is connected to Team: <Badge color="blue" size="xs" class="mt-2">{teams_team_name}</Badge>
|
||||
This workspace is connected to Team: <Badge color="blue" size="xs" class="mt-2"
|
||||
>{teams_team_name}</Badge
|
||||
>
|
||||
</p>
|
||||
<Tooltip text={teams_team_name}>
|
||||
Each workspace can only be connected to one Microsoft Teams team. You can configure it under <a
|
||||
@@ -499,7 +506,7 @@
|
||||
{:else}
|
||||
<Button
|
||||
disabled={emptyString(handlerExtraArgs['channel'])}
|
||||
btnClasses="w-32 text-center"
|
||||
btnClasses="w-32 text-center mt-2"
|
||||
color="dark"
|
||||
on:click={() => sendTeamsMessage(handlerExtraArgs['channel'] ?? '')}
|
||||
size="xs">Send test message</Button
|
||||
@@ -507,7 +514,7 @@
|
||||
{#if connectionTestJob !== undefined}
|
||||
<p class="text-normal text-2xs mt-1 gap-2">
|
||||
{#if connectionTestJob.in_progress}
|
||||
<RotateCw size={14} />
|
||||
<RotateCw size={14} class="animate-spin" />
|
||||
{:else if connectionTestJob.is_success}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import TeamSelector from './TeamSelector.svelte'
|
||||
import ChannelSelector from './ChannelSelector.svelte'
|
||||
|
||||
export let setting: Setting
|
||||
export let version: string
|
||||
@@ -134,9 +136,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleTeamChange(event: Event, i: number) {
|
||||
const teamId = (event.target as HTMLSelectElement).value
|
||||
const team = $values['teams'].find((team) => team.team_id === teamId) || null
|
||||
function handleTeamChange(teamItem: { team_id: string; team_name: string }, i: number) {
|
||||
const team = $values['teams'].find((team) => team.team_id === teamItem.team_id) || null
|
||||
$values['critical_error_channels'][i] = {
|
||||
teams_channel: {
|
||||
team_id: team?.team_id,
|
||||
@@ -147,13 +148,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleChannelChange(event: Event, setting: Setting, i: number) {
|
||||
const channelId = (event.target as HTMLSelectElement).value
|
||||
const team = $values['teams'].find(
|
||||
(team) => team.team_id === $values['critical_error_channels'][i]?.teams_channel?.team_id
|
||||
)
|
||||
const channel = team?.channels.find((channel) => channel.channel_id === channelId) || null
|
||||
if (channelId) {
|
||||
function handleChannelChange(channel: { channel_id: string; channel_name: string }, i: number) {
|
||||
const team = $values['critical_error_channels'][i]?.teams_channel
|
||||
if (team) {
|
||||
$values['critical_error_channels'][i] = {
|
||||
teams_channel: {
|
||||
team_id: team?.team_id,
|
||||
@@ -458,46 +455,45 @@
|
||||
/>
|
||||
{:else if v && 'teams_channel' in v}
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<select on:change={(e) => handleTeamChange(e, i)}>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
selected={!$values['critical_error_channels'][i]?.teams_channel
|
||||
?.team_id}>Select team</option
|
||||
>
|
||||
{#if $values['teams']}
|
||||
{#each $values['teams'] as team}
|
||||
<option
|
||||
value={team.team_id}
|
||||
selected={$values['critical_error_channels'][i]?.teams_channel
|
||||
?.team_id === team.team_id}
|
||||
>
|
||||
{team.team_name}
|
||||
</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<TeamSelector
|
||||
containerClass="w-44"
|
||||
minWidth="140px"
|
||||
showRefreshButton={false}
|
||||
placeholder="Select team"
|
||||
teams={$values['teams']}
|
||||
on:change={(e) => handleTeamChange(e.detail, i)}
|
||||
selectedTeam={$values['critical_error_channels'][i]?.teams_channel
|
||||
? {
|
||||
team_id:
|
||||
$values['critical_error_channels'][i]?.teams_channel?.team_id,
|
||||
team_name:
|
||||
$values['critical_error_channels'][i]?.teams_channel?.team_name
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
|
||||
{#if $values['critical_error_channels'][i]?.teams_channel?.team_id}
|
||||
<select
|
||||
id="channel-select"
|
||||
on:change={(e) => handleChannelChange(e, setting, i)}
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
selected={!$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id}>Select channel</option
|
||||
>
|
||||
{#each $values['teams'].find((team) => team.team_id === $values['critical_error_channels'][i]?.teams_channel?.team_id)?.channels ?? [] as channel}
|
||||
<option
|
||||
value={channel.channel_id}
|
||||
selected={$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id === channel.channel_id}
|
||||
>
|
||||
{channel.channel_name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ChannelSelector
|
||||
containerClass=""
|
||||
placeholder="Select channel"
|
||||
channels={$values['teams'].find(
|
||||
(team) =>
|
||||
team.team_id ===
|
||||
$values['critical_error_channels'][i]?.teams_channel?.team_id
|
||||
)?.channels ?? []}
|
||||
on:change={(e) => handleChannelChange(e.detail, i)}
|
||||
selectedChannel={$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id
|
||||
? {
|
||||
channel_id:
|
||||
$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id,
|
||||
channel_name:
|
||||
$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_name
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
{/if}
|
||||
<div>
|
||||
<button on:click={fetchTeams} class="flex items-center gap-1 mt-2">
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import Select from '$lib/components/apps/svelte-select/lib/Select.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import { RefreshCcw } from 'lucide-svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
|
||||
interface TeamItem {
|
||||
team_id: string
|
||||
team_name: string
|
||||
}
|
||||
|
||||
export let disabled = false
|
||||
export let placeholder = 'Select a team'
|
||||
export let selectedTeam: TeamItem | undefined = undefined
|
||||
export let containerClass = 'w-64'
|
||||
export let showRefreshButton = true
|
||||
export let teams: TeamItem[] | undefined = undefined
|
||||
export let minWidth = '160px'
|
||||
|
||||
let isFetching = false
|
||||
let darkMode: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: TeamItem
|
||||
error: Error
|
||||
}>()
|
||||
|
||||
function onThemeChange() {
|
||||
darkMode = document.documentElement.classList.contains('dark')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onThemeChange()
|
||||
if (!teams) {
|
||||
loadTeams()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadTeams() {
|
||||
isFetching = true
|
||||
try {
|
||||
const response = (await WorkspaceService.listAvailableTeamsIds({
|
||||
workspace: $workspaceStore!
|
||||
})) as unknown as TeamItem[]
|
||||
|
||||
teams = response || []
|
||||
isFetching = false
|
||||
console.log('Teams loaded:', teams.length)
|
||||
return teams
|
||||
} catch (error) {
|
||||
isFetching = false
|
||||
dispatch('error', error)
|
||||
console.error('Error loading teams:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function handleTeamSelect(event) {
|
||||
selectedTeam = event.detail
|
||||
if (selectedTeam) {
|
||||
dispatch('change', selectedTeam)
|
||||
}
|
||||
}
|
||||
|
||||
function filterTeams(filterText: string | unknown) {
|
||||
if (!teams) return teams
|
||||
|
||||
const searchText = typeof filterText === 'string' ? filterText : ''
|
||||
|
||||
const filtered = searchText
|
||||
? teams.filter((team) => team.team_name.toLowerCase().includes(searchText.toLowerCase()))
|
||||
: teams
|
||||
|
||||
return filtered
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={containerClass}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-grow" style="min-width: {minWidth};">
|
||||
<Select
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={'border-color: lightgray; min-width: ' +
|
||||
minWidth +
|
||||
';' +
|
||||
(darkMode
|
||||
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
|
||||
: SELECT_INPUT_DEFAULT_STYLE.containerStyles)}
|
||||
itemId="team_id"
|
||||
label="team_name"
|
||||
items={teams || []}
|
||||
on:change={handleTeamSelect}
|
||||
{placeholder}
|
||||
searchable={true}
|
||||
loading={isFetching}
|
||||
disabled={disabled || isFetching}
|
||||
on:input={(e) => filterTeams(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if showRefreshButton}
|
||||
<button
|
||||
on:click={loadTeams}
|
||||
disabled={isFetching || disabled}
|
||||
class="flex items-center justify-center p-1.5 rounded hover:bg-surface-hover focus:bg-surface-hover disabled:opacity-50"
|
||||
title="Refresh teams from Microsoft"
|
||||
>
|
||||
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isFetching || ((!teams || teams.length === 0) && !isFetching)}
|
||||
<div class="text-xs text-tertiary mt-1">
|
||||
{#if isFetching}
|
||||
Fetching teams from Microsoft...
|
||||
{:else}
|
||||
No available teams found
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<DarkModeObserver on:change={onThemeChange} />
|
||||
Reference in New Issue
Block a user