mirror of
https://github.com/langgenius/dify.git
synced 2026-05-26 20:07:46 +08:00
Compare commits
2 Commits
main
...
codex/alig
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a06bb45b3 | |||
| 34b19422a2 |
@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { Virtualizer } from '@tanstack/react-virtual'
|
||||
import type { RefObject } from 'react'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState, useTransition } from 'react'
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteClear,
|
||||
@ -159,6 +159,29 @@ const virtualizedSuggestions: Suggestion[] = Array.from({ length: 1000 }, (_, in
|
||||
|
||||
const getSuggestionLabel = (item: Suggestion) => item.label
|
||||
|
||||
async function searchSuggestions(
|
||||
suggestions: Suggestion[],
|
||||
query: string,
|
||||
filter: (item: string, query: string) => boolean,
|
||||
): Promise<{ items: Suggestion[], error: string | null }> {
|
||||
await new Promise(resolve => window.setTimeout(resolve, 500))
|
||||
|
||||
if (query === 'will_error') {
|
||||
return {
|
||||
items: [],
|
||||
error: 'Failed to load suggestions. Please try again.',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items: suggestions.filter(item => (
|
||||
filter(item.label, query)
|
||||
|| (item.description ? filter(item.description, query) : false)
|
||||
)),
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
const SuggestionItem = ({
|
||||
item,
|
||||
dense,
|
||||
@ -227,6 +250,7 @@ const BasicTagAutocomplete = ({
|
||||
<Autocomplete
|
||||
items={tagSuggestions}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
mode="list"
|
||||
openOnInputClick
|
||||
>
|
||||
<AutocompleteInputGroup size={size}>
|
||||
@ -311,32 +335,64 @@ const LimitedStatus = ({
|
||||
}
|
||||
|
||||
const AsyncSearchDemo = () => {
|
||||
const [value, setValue] = useState('agent')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [items, setItems] = useState(remoteSuggestions)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [searchResults, setSearchResults] = useState<Suggestion[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const { contains } = useAutocompleteFilter()
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
const timeout = window.setTimeout(() => {
|
||||
setItems(
|
||||
value.trim()
|
||||
? remoteSuggestions.filter(item => item.label.toLowerCase().includes(value.trim().toLowerCase()))
|
||||
: remoteSuggestions,
|
||||
)
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
const status = (() => {
|
||||
if (isPending)
|
||||
return 'Searching remote suggestions…'
|
||||
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [value])
|
||||
if (error)
|
||||
return error
|
||||
|
||||
if (searchValue === '')
|
||||
return null
|
||||
|
||||
if (searchResults.length === 0)
|
||||
return `No remote suggestion matches "${searchValue}".`
|
||||
|
||||
return `${searchResults.length} remote suggestion${searchResults.length === 1 ? '' : 's'} found`
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className={inputWidth}>
|
||||
<Autocomplete
|
||||
items={items}
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
items={searchResults}
|
||||
value={searchValue}
|
||||
onValueChange={(nextSearchValue) => {
|
||||
setSearchValue(nextSearchValue)
|
||||
|
||||
const controller = new AbortController()
|
||||
abortControllerRef.current?.abort()
|
||||
abortControllerRef.current = controller
|
||||
|
||||
if (nextSearchValue === '') {
|
||||
setSearchResults([])
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
setError(null)
|
||||
|
||||
const result = await searchSuggestions(remoteSuggestions, nextSearchValue, contains)
|
||||
|
||||
if (controller.signal.aborted)
|
||||
return
|
||||
|
||||
startTransition(() => {
|
||||
setSearchResults(result.items)
|
||||
setError(result.error)
|
||||
})
|
||||
})
|
||||
}}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
openOnInputClick
|
||||
filter={null}
|
||||
mode="list"
|
||||
>
|
||||
<AutocompleteInputGroup>
|
||||
<span className="i-ri-cloud-line ml-2 size-4 shrink-0 text-text-tertiary" aria-hidden="true" />
|
||||
@ -344,16 +400,15 @@ const AsyncSearchDemo = () => {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteContent portalProps={{ hidden: !status }} popupProps={{ 'aria-busy': isPending || undefined }}>
|
||||
<AutocompleteStatus>
|
||||
{loading ? 'Loading suggestions…' : `${items.length} remote suggestions`}
|
||||
{status}
|
||||
</AutocompleteStatus>
|
||||
<AutocompleteList>
|
||||
{(item: Suggestion) => (
|
||||
<SuggestionItem key={item.value} item={item} />
|
||||
)}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No remote suggestion. Keep the typed query.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
@ -467,6 +522,7 @@ const FuzzyMatchingDemo = () => {
|
||||
onValueChange={setValue}
|
||||
filter={contains}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
mode="list"
|
||||
openOnInputClick
|
||||
>
|
||||
<AutocompleteInputGroup>
|
||||
@ -567,6 +623,7 @@ export const GroupedSuggestions: Story = {
|
||||
<Autocomplete
|
||||
items={groupedSuggestions}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
mode="list"
|
||||
openOnInputClick
|
||||
>
|
||||
<AutocompleteInputGroup>
|
||||
@ -595,6 +652,7 @@ export const LimitResults: Story = {
|
||||
items={workflowSuggestions}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
limit={5}
|
||||
mode="list"
|
||||
openOnInputClick
|
||||
>
|
||||
<AutocompleteInputGroup>
|
||||
@ -627,6 +685,7 @@ export const CommandPalette: Story = {
|
||||
inline
|
||||
items={commandGroups}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
mode="list"
|
||||
autoHighlight="always"
|
||||
keepHighlight
|
||||
>
|
||||
@ -649,6 +708,7 @@ const VirtualizedLongSuggestionsDemo = () => {
|
||||
<Autocomplete
|
||||
items={virtualizedSuggestions}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
mode="list"
|
||||
virtualized
|
||||
openOnInputClick
|
||||
onItemHighlighted={(item, details) => {
|
||||
@ -686,6 +746,7 @@ export const Empty: Story = {
|
||||
items={tagSuggestions}
|
||||
itemToStringValue={getSuggestionLabel}
|
||||
defaultValue="private-release-note"
|
||||
mode="list"
|
||||
openOnInputClick
|
||||
>
|
||||
<AutocompleteInputGroup>
|
||||
@ -710,7 +771,7 @@ export const Empty: Story = {
|
||||
export const DisabledAndReadOnly: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-80 flex-col gap-3">
|
||||
<Autocomplete items={tagSuggestions} itemToStringValue={getSuggestionLabel} defaultValue="feature" disabled>
|
||||
<Autocomplete items={tagSuggestions} itemToStringValue={getSuggestionLabel} defaultValue="feature" mode="list" disabled>
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput aria-label="Disabled tag autocomplete" />
|
||||
<AutocompleteClear />
|
||||
@ -724,7 +785,7 @@ export const DisabledAndReadOnly: Story = {
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
<Autocomplete items={promptCompletions} itemToStringValue={getSuggestionLabel} defaultValue="summarize this conversation" readOnly>
|
||||
<Autocomplete items={promptCompletions} itemToStringValue={getSuggestionLabel} defaultValue="summarize this conversation" mode="both" readOnly>
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput aria-label="Read-only prompt autocomplete" />
|
||||
<AutocompleteClear />
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { Virtualizer } from '@tanstack/react-virtual'
|
||||
import type { RefObject } from 'react'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState, useTransition } from 'react'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
@ -26,6 +26,7 @@ import {
|
||||
ComboboxStatus,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxFilter,
|
||||
useComboboxFilteredItems,
|
||||
} from '.'
|
||||
import { cn } from '../cn'
|
||||
@ -178,8 +179,34 @@ const defaultPopupDataSource = dataSourceOptions[1]!
|
||||
const readOnlyDataSource = dataSourceOptions[2]!
|
||||
const defaultTool = toolGroups[0]!.items[0]!
|
||||
const defaultReviewers = [reviewerOptions[0]!, reviewerOptions[1]!]
|
||||
const defaultAsyncReviewers = [reviewerOptions[1]!]
|
||||
const defaultTag = tagOptions[2]!
|
||||
|
||||
const getOptionLabel = (option: Option) => option.label
|
||||
|
||||
async function searchOptions(
|
||||
options: Option[],
|
||||
query: string,
|
||||
filter: (item: string, query: string) => boolean,
|
||||
): Promise<{ items: Option[], error: string | null }> {
|
||||
await new Promise(resolve => window.setTimeout(resolve, 450))
|
||||
|
||||
if (query === 'will_error') {
|
||||
return {
|
||||
items: [],
|
||||
error: 'Failed to fetch matches. Please try again.',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items: options.filter(option => (
|
||||
filter(option.label, query)
|
||||
|| (option.meta ? filter(option.meta, query) : false)
|
||||
)),
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
const renderOptionItem = (option: Option) => (
|
||||
<ComboboxItem key={option.value} value={option} disabled={option.disabled} className="h-auto min-h-8 py-1.5">
|
||||
<ComboboxItemText className="flex items-center gap-2 px-0">
|
||||
@ -348,35 +375,88 @@ const VirtualizedLongListDemo = () => {
|
||||
}
|
||||
|
||||
const AsyncDirectoryDemo = () => {
|
||||
const [inputValue, setInputValue] = useState('ma')
|
||||
const [value, setValue] = useState<Option | null>(null)
|
||||
const [items, setItems] = useState(directoryOptions.slice(0, 3))
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<Option[]>([])
|
||||
const [selectedValue, setSelectedValue] = useState<Option | null>(null)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const { contains } = useComboboxFilter()
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const trimmedSearchValue = searchValue.trim()
|
||||
const items = useMemo(() => {
|
||||
if (!selectedValue || searchResults.some(option => option.value === selectedValue.value))
|
||||
return searchResults
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
const timeout = window.setTimeout(() => {
|
||||
const query = inputValue.trim().toLowerCase()
|
||||
setItems(
|
||||
query
|
||||
? directoryOptions.filter(option => `${option.label} ${option.meta}`.toLowerCase().includes(query))
|
||||
: directoryOptions.slice(0, 5),
|
||||
)
|
||||
setLoading(false)
|
||||
}, 450)
|
||||
return [...searchResults, selectedValue]
|
||||
}, [searchResults, selectedValue])
|
||||
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [inputValue])
|
||||
const status = (() => {
|
||||
if (isPending)
|
||||
return 'Searching directory matches…'
|
||||
|
||||
if (error)
|
||||
return error
|
||||
|
||||
if (trimmedSearchValue === '')
|
||||
return selectedValue ? null : 'Start typing to search owners…'
|
||||
|
||||
if (searchResults.length === 0)
|
||||
return `No matches for "${trimmedSearchValue}".`
|
||||
|
||||
return `${searchResults.length} owner${searchResults.length === 1 ? '' : 's'} found`
|
||||
})()
|
||||
|
||||
const emptyMessage = trimmedSearchValue === '' || isPending || searchResults.length > 0 || error
|
||||
? null
|
||||
: 'Try a different owner search.'
|
||||
|
||||
return (
|
||||
<FieldRoot name="owner" className={fieldWidth}>
|
||||
<FieldLabel>Owner</FieldLabel>
|
||||
<Combobox
|
||||
items={value && !items.some(item => item.value === value.value) ? [value, ...items] : items}
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
inputValue={inputValue}
|
||||
onInputValueChange={setInputValue}
|
||||
items={items}
|
||||
itemToStringLabel={getOptionLabel}
|
||||
filter={null}
|
||||
value={selectedValue}
|
||||
onOpenChangeComplete={(open) => {
|
||||
if (!open && selectedValue)
|
||||
setSearchResults([selectedValue])
|
||||
}}
|
||||
onValueChange={(nextSelectedValue) => {
|
||||
setSelectedValue(nextSelectedValue)
|
||||
setSearchValue('')
|
||||
setError(null)
|
||||
}}
|
||||
onInputValueChange={(nextSearchValue, { reason }) => {
|
||||
setSearchValue(nextSearchValue)
|
||||
|
||||
if (nextSearchValue === '') {
|
||||
setSearchResults([])
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (reason === 'item-press')
|
||||
return
|
||||
|
||||
const controller = new AbortController()
|
||||
abortControllerRef.current?.abort()
|
||||
abortControllerRef.current = controller
|
||||
|
||||
startTransition(async () => {
|
||||
setError(null)
|
||||
|
||||
const result = await searchOptions(directoryOptions, nextSearchValue, contains)
|
||||
|
||||
if (controller.signal.aborted)
|
||||
return
|
||||
|
||||
startTransition(() => {
|
||||
setSearchResults(result.items)
|
||||
setError(result.error)
|
||||
})
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ComboboxInputGroup className="h-8 min-h-8 px-2">
|
||||
<span aria-hidden className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" />
|
||||
@ -384,12 +464,12 @@ const AsyncDirectoryDemo = () => {
|
||||
<ComboboxClear className="mr-0.5" />
|
||||
<ComboboxInputTrigger className="mr-0" />
|
||||
</ComboboxInputGroup>
|
||||
<ComboboxContent popupClassName="w-[420px]">
|
||||
<ComboboxContent popupClassName="w-[420px]" popupProps={{ 'aria-busy': isPending || undefined }}>
|
||||
<ComboboxStatus className="border-b border-divider-subtle">
|
||||
{loading ? 'Loading directory matches…' : `${items.length} selectable owners`}
|
||||
{status}
|
||||
</ComboboxStatus>
|
||||
<ComboboxList>{renderOptionItem}</ComboboxList>
|
||||
<ComboboxEmpty>No owner matches this query</ComboboxEmpty>
|
||||
<ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</FieldRoot>
|
||||
@ -397,38 +477,111 @@ const AsyncDirectoryDemo = () => {
|
||||
}
|
||||
|
||||
const AsyncReviewerDemo = () => {
|
||||
const [inputValue, setInputValue] = useState('ma')
|
||||
const [value, setValue] = useState<Option[]>([reviewerOptions[1]!])
|
||||
const [items, setItems] = useState(reviewerOptions.slice(0, 3))
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<Option[]>([])
|
||||
const [selectedValues, setSelectedValues] = useState<Option[]>(defaultAsyncReviewers)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [blockStartStatus, setBlockStartStatus] = useState(false)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const { contains } = useComboboxFilter()
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const selectedValuesRef = useRef<Option[]>(defaultAsyncReviewers)
|
||||
const trimmedSearchValue = searchValue.trim()
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
const timeout = window.setTimeout(() => {
|
||||
const query = inputValue.trim().toLowerCase()
|
||||
const matches = query
|
||||
? reviewerOptions.filter(option => `${option.label} ${option.meta}`.toLowerCase().includes(query))
|
||||
: reviewerOptions
|
||||
const items = useMemo(() => {
|
||||
if (selectedValues.length === 0)
|
||||
return searchResults
|
||||
|
||||
setItems(matches)
|
||||
setLoading(false)
|
||||
}, 450)
|
||||
const merged = [...searchResults]
|
||||
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [inputValue])
|
||||
selectedValues.forEach((selected) => {
|
||||
if (!searchResults.some(result => result.value === selected.value))
|
||||
merged.push(selected)
|
||||
})
|
||||
|
||||
const selectedItems = value.filter(selected => !items.some(item => item.value === selected.value))
|
||||
return merged
|
||||
}, [searchResults, selectedValues])
|
||||
|
||||
const status = (() => {
|
||||
if (isPending)
|
||||
return 'Searching reviewer matches…'
|
||||
|
||||
if (error)
|
||||
return error
|
||||
|
||||
if (trimmedSearchValue === '' && !blockStartStatus)
|
||||
return selectedValues.length > 0 ? null : 'Start typing to search reviewers…'
|
||||
|
||||
if (searchResults.length === 0 && !blockStartStatus)
|
||||
return `No matches for "${trimmedSearchValue}".`
|
||||
|
||||
return `${searchResults.length} reviewer${searchResults.length === 1 ? '' : 's'} found`
|
||||
})()
|
||||
|
||||
const emptyMessage = trimmedSearchValue === '' || isPending || searchResults.length > 0 || error
|
||||
? null
|
||||
: 'Try a different reviewer search.'
|
||||
|
||||
return (
|
||||
<FieldRoot name="asyncReviewers" className={fieldWidth}>
|
||||
<FieldLabel>Async reviewers</FieldLabel>
|
||||
<Combobox
|
||||
items={[...selectedItems, ...items]}
|
||||
items={items}
|
||||
itemToStringLabel={getOptionLabel}
|
||||
multiple
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
inputValue={inputValue}
|
||||
onInputValueChange={setInputValue}
|
||||
filter={null}
|
||||
value={selectedValues}
|
||||
onOpenChangeComplete={(open) => {
|
||||
if (!open) {
|
||||
setSearchResults(selectedValuesRef.current)
|
||||
setBlockStartStatus(false)
|
||||
}
|
||||
}}
|
||||
onValueChange={(nextSelectedValues) => {
|
||||
selectedValuesRef.current = nextSelectedValues
|
||||
setSelectedValues(nextSelectedValues)
|
||||
setSearchValue('')
|
||||
setError(null)
|
||||
|
||||
if (nextSelectedValues.length === 0) {
|
||||
setSearchResults([])
|
||||
setBlockStartStatus(false)
|
||||
}
|
||||
else {
|
||||
setBlockStartStatus(true)
|
||||
}
|
||||
}}
|
||||
onInputValueChange={(nextSearchValue, { reason }) => {
|
||||
setSearchValue(nextSearchValue)
|
||||
|
||||
const controller = new AbortController()
|
||||
abortControllerRef.current?.abort()
|
||||
abortControllerRef.current = controller
|
||||
|
||||
if (nextSearchValue === '') {
|
||||
setSearchResults(selectedValuesRef.current)
|
||||
setError(null)
|
||||
setBlockStartStatus(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (reason === 'item-press')
|
||||
return
|
||||
|
||||
startTransition(async () => {
|
||||
setError(null)
|
||||
|
||||
const result = await searchOptions(reviewerOptions, nextSearchValue, contains)
|
||||
|
||||
if (controller.signal.aborted)
|
||||
return
|
||||
|
||||
startTransition(() => {
|
||||
setSearchResults(result.items)
|
||||
setError(result.error)
|
||||
})
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ComboboxInputGroup className="h-auto min-h-8 items-start py-1">
|
||||
<ComboboxChips>
|
||||
@ -447,12 +600,12 @@ const AsyncReviewerDemo = () => {
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
</ComboboxInputGroup>
|
||||
<ComboboxContent popupClassName="w-[420px]">
|
||||
<ComboboxContent popupClassName="w-[420px]" popupProps={{ 'aria-busy': isPending || undefined }}>
|
||||
<ComboboxStatus className="border-b border-divider-subtle">
|
||||
{loading ? 'Loading reviewer matches…' : `${items.length} selectable reviewers`}
|
||||
{status}
|
||||
</ComboboxStatus>
|
||||
<ComboboxList>{renderOptionItem}</ComboboxList>
|
||||
<ComboboxEmpty>No reviewer matches this query</ComboboxEmpty>
|
||||
<ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<FieldDescription>Selected reviewers stay available while async matches change.</FieldDescription>
|
||||
|
||||
@ -17,7 +17,7 @@ const Placeholder = ({
|
||||
loadingFileName,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className={wrapClassName}>
|
||||
<div className={cn(wrapClassName, 'p-3')}>
|
||||
<SkeletonRow>
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center gap-2 rounded-[10px] border-[0.5px]
|
||||
|
||||
@ -30,7 +30,7 @@ const Installed: FC<Props> = ({
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="flex flex-col items-start justify-center gap-2 self-stretch px-6 py-3">
|
||||
<p className="system-md-regular text-text-secondary">{(isFailed && errMsg) ? errMsg : t(`installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`, { ns: 'plugin' })}</p>
|
||||
{payload && (
|
||||
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
|
||||
|
||||
@ -116,7 +116,7 @@ const Installed: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="flex flex-col items-start justify-center gap-2 self-stretch px-6 py-3">
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
|
||||
<p>
|
||||
|
||||
@ -106,7 +106,7 @@ const Uploading: FC<Props> = ({
|
||||
}, [handleUpload])
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
|
||||
<div className="flex flex-col items-start justify-center gap-2 self-stretch px-6 py-3">
|
||||
<div className="flex items-center gap-1 self-stretch">
|
||||
<span className="i-ri-loader-2-line size-4 animate-spin-slow text-text-accent" />
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
|
||||
Reference in New Issue
Block a user