mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 13:26:58 +08:00
refactor(web): unify theme handling behind a theme feature module (#45689)
This commit is contained in:
166 files changed
+635
-968
No files matched your search
Generated
-93
@@ -602,11 +602,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/create-app-modal/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"web/app/components/app/create-from-dsl-modal/uploader.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 4
|
||||
@@ -642,11 +637,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/deploy/shared/deployment-configuration/credential-field.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/app/deploy/shared/deployment-configuration/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
@@ -860,9 +850,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/base/audio-gallery/AudioPlayer.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/media-has-caption": {
|
||||
"count": 1
|
||||
},
|
||||
@@ -1402,9 +1389,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/base/markdown-blocks/code-block.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
@@ -1808,11 +1792,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/priority-label/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
@@ -2000,11 +1979,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/create/file-uploader/components/file-list-item.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
@@ -2240,9 +2214,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/file-list-item.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 2
|
||||
},
|
||||
@@ -2406,9 +2377,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
},
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
},
|
||||
@@ -2917,11 +2885,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/card/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/install-plugin/base/loading-error.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
@@ -3130,11 +3093,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 4
|
||||
@@ -3238,18 +3196,10 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-item/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 9
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/category-marketplace-panel.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/empty/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
@@ -3330,11 +3280,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/provider-card.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/readme-panel/content.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
@@ -3848,31 +3793,16 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tool/action-item.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tool/tool.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 14
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/trigger-plugin/item.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/collaboration/components/user-cursors.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
@@ -4131,9 +4061,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
},
|
||||
"react/only-export-components": {
|
||||
"count": 1
|
||||
},
|
||||
@@ -4959,11 +4886,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-importer.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
@@ -5779,11 +5701,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/status-container.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/status.tsx": {
|
||||
"jsx-a11y/anchor-has-content": {
|
||||
"count": 1
|
||||
@@ -6093,11 +6010,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/item.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 9
|
||||
}
|
||||
},
|
||||
"web/features/agent-v2/agent-detail/configure/components/preview/build-background.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 2
|
||||
@@ -6183,11 +6095,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/features/app-access-error/page.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/features/app-access-error/trial-boundary.tsx": {
|
||||
"better-tailwindcss/enforce-logical-properties": {
|
||||
"count": 1
|
||||
|
||||
@@ -70,6 +70,10 @@ const webRestrictedImports = [
|
||||
importNames: ['useLocalStorageState'],
|
||||
message: 'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
|
||||
},
|
||||
{
|
||||
name: 'next-themes',
|
||||
message: 'Use the @/features/theme module instead of next-themes.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
@@ -1809,6 +1813,19 @@ export const lintConfig = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['web/features/theme/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
...webRestrictedImports[1],
|
||||
paths: webRestrictedImports[1].paths.filter((path) => path.name !== 'next-themes'),
|
||||
patterns: [...webRestrictedImports[1].patterns, rawHttpImports],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
...handwrittenApiScope,
|
||||
excludeFiles: [...handwrittenApiScope.excludeFiles, ...marketplaceBoundaryFiles],
|
||||
|
||||
@@ -29,7 +29,7 @@ import { toast } from '@/app/notifications'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
@@ -246,7 +246,7 @@ function CreateApp({ onClose, onCreateFromTemplate, defaultAppMode }: CreateAppP
|
||||
<div className="mb-2 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center border-0 bg-transparent p-0 text-left focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="flex cursor-pointer items-center border-0 bg-transparent p-0 text-start focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setIsAppTypeExpanded(!isAppTypeExpanded)}
|
||||
>
|
||||
<span className="system-2xs-medium-uppercase text-text-tertiary">
|
||||
@@ -254,7 +254,7 @@ function CreateApp({ onClose, onCreateFromTemplate, defaultAppMode }: CreateAppP
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`ml-1 i-ri-arrow-right-s-line size-4 text-text-tertiary transition-transform ${isAppTypeExpanded ? 'rotate-90' : ''}`}
|
||||
className={`ms-1 i-ri-arrow-right-s-line size-4 text-text-tertiary transition-transform ${isAppTypeExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -344,7 +344,7 @@ function CreateApp({ onClose, onCreateFromTemplate, defaultAppMode }: CreateAppP
|
||||
>
|
||||
{t(($) => $['newApp.captionDescription'], { ns: 'app' })}
|
||||
</label>
|
||||
<span className="ml-1 system-xs-regular text-text-tertiary">
|
||||
<span className="ms-1 system-xs-regular text-text-tertiary">
|
||||
({t(($) => $['newApp.optional'], { ns: 'app' })})
|
||||
</span>
|
||||
</div>
|
||||
@@ -361,7 +361,7 @@ function CreateApp({ onClose, onCreateFromTemplate, defaultAppMode }: CreateAppP
|
||||
<div className="flex items-center justify-between pt-5 pb-10">
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left system-xs-regular text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="flex cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-start system-xs-regular text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCreateFromTemplate}
|
||||
>
|
||||
<span>{t(($) => $['newApp.noIdeaTip'], { ns: 'app' })}</span>
|
||||
@@ -391,7 +391,7 @@ function CreateApp({ onClose, onCreateFromTemplate, defaultAppMode }: CreateAppP
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex h-full flex-1 shrink justify-start overflow-hidden">
|
||||
<div className="absolute top-0 right-0 left-0 h-6 border-b border-b-divider-subtle 2xl:h-34.75"></div>
|
||||
<div className="absolute inset-s-0 inset-e-0 top-0 h-6 border-b border-b-divider-subtle 2xl:h-34.75"></div>
|
||||
<div className="max-w-190 border-x border-x-divider-subtle">
|
||||
<div className="h-6 2xl:h-34.75" />
|
||||
<AppPreview mode={appMode} />
|
||||
@@ -457,7 +457,7 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'relative box-content h-21 w-47.75 cursor-pointer rounded-xl border-[0.5px] border-components-option-card-option-border bg-components-panel-on-panel-item-bg p-3 text-left shadow-xs outline-hidden hover:shadow-md focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
'relative box-content h-21 w-47.75 cursor-pointer rounded-xl border-[0.5px] border-components-option-card-option-border bg-components-panel-on-panel-item-bg p-3 text-start shadow-xs outline-hidden hover:shadow-md focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
active
|
||||
? 'shadow-md outline-[1.5px] outline-components-option-card-option-selected-border outline-solid'
|
||||
: '',
|
||||
@@ -520,7 +520,7 @@ function AppPreview({ mode }: { mode: AppType }) {
|
||||
}
|
||||
|
||||
function AppScreenShot({ mode, show }: { mode: AppType; show: boolean }) {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const modeToImageMap = {
|
||||
chat: 'Chatbot',
|
||||
'advanced-chat': 'Chatflow',
|
||||
|
||||
@@ -15,8 +15,7 @@ import {
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { credentialFieldId } from './utils/field-id'
|
||||
import { credentialProviderName } from './utils/workflow-deployment-input'
|
||||
import { WorkflowDependencyPreview } from './workflow-source-popover'
|
||||
@@ -35,11 +34,10 @@ export function CredentialField({
|
||||
const { t } = useTranslation(['deployments'])
|
||||
const { t: tPlugin } = useTranslation(['plugin'])
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const inputId = credentialFieldId(slot)
|
||||
const name = credentialProviderName(slot.provider_id)
|
||||
const iconFileName =
|
||||
theme === Theme.dark ? slot.icon_dark || slot.icon : slot.icon || slot.icon_dark
|
||||
const iconFileName = theme === 'dark' ? slot.icon_dark || slot.icon : slot.icon || slot.icon_dark
|
||||
const iconSrc = iconFileName ? getIconUrl(iconFileName) : undefined
|
||||
const category =
|
||||
slot.category === PluginCategory.PLUGIN_CATEGORY_MODEL
|
||||
@@ -113,7 +111,7 @@ export function CredentialField({
|
||||
<SelectItem
|
||||
key={candidate.credential_id}
|
||||
value={candidate.credential_id}
|
||||
className="h-8 gap-1.5 py-1 pr-1 pl-3 system-md-regular data-selected:bg-state-base-hover"
|
||||
className="h-8 gap-1.5 py-1 ps-3 pe-1 system-md-regular data-selected:bg-state-base-hover"
|
||||
>
|
||||
<span className="flex size-2 shrink-0 items-center justify-center">
|
||||
<StatusDot size="small" />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallback } from 'react'
|
||||
import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select'
|
||||
import { findNodeIcon } from '@/app/components/workflow/hooks/use-tool-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
|
||||
@@ -60,7 +60,7 @@ export const useGetProviderIcon = (nodes: UnsupportedNode[]) => {
|
||||
}),
|
||||
)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins(hasTriggerPluginNode)
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
return useCallback(
|
||||
(node: UnsupportedNode) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ResolvedTheme } from '@/features/theme/theme'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
import { Theme } from '@/types/app'
|
||||
import TriggerByDisplay from '../trigger-by-display'
|
||||
|
||||
let theme = Theme.light
|
||||
let theme: ResolvedTheme = 'light'
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme }),
|
||||
vi.mock('@/features/theme/hooks', () => ({
|
||||
useResolvedTheme: () => theme,
|
||||
}))
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: ({ toolIcon }: { toolIcon?: string }) => (
|
||||
@@ -17,7 +17,7 @@ vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
|
||||
describe('TriggerByDisplay', () => {
|
||||
beforeEach(() => {
|
||||
theme = Theme.light
|
||||
theme = 'light'
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -46,7 +46,7 @@ describe('TriggerByDisplay', () => {
|
||||
})
|
||||
|
||||
it('uses the theme-specific plugin icon when available', () => {
|
||||
theme = Theme.dark
|
||||
theme = 'dark'
|
||||
render(
|
||||
<TriggerByDisplay
|
||||
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { FC } from 'react'
|
||||
import type { ResolvedTheme } from '@/features/theme/theme'
|
||||
import type { TriggerMetadata } from '@/models/log'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
type TriggerByDisplayProps = {
|
||||
triggeredFrom: WorkflowRunTriggeredFrom
|
||||
@@ -39,11 +39,11 @@ const getTriggerDisplayName = (
|
||||
return nameMap[triggeredFrom] || triggeredFrom
|
||||
}
|
||||
|
||||
const getPluginIcon = (metadata: TriggerMetadata | undefined, theme: Theme) => {
|
||||
const getPluginIcon = (metadata: TriggerMetadata | undefined, theme: ResolvedTheme) => {
|
||||
if (!metadata) return null
|
||||
|
||||
const icon =
|
||||
theme === Theme.dark ? metadata.icon_dark || metadata.icon : metadata.icon || metadata.icon_dark
|
||||
theme === 'dark' ? metadata.icon_dark || metadata.icon : metadata.icon || metadata.icon_dark
|
||||
|
||||
if (!icon) return null
|
||||
|
||||
@@ -53,7 +53,7 @@ const getPluginIcon = (metadata: TriggerMetadata | undefined, theme: Theme) => {
|
||||
const getTriggerIcon = (
|
||||
triggeredFrom: WorkflowRunTriggeredFrom,
|
||||
metadata: TriggerMetadata | undefined,
|
||||
theme: Theme,
|
||||
theme: ResolvedTheme,
|
||||
) => {
|
||||
switch (triggeredFrom) {
|
||||
case 'webhook':
|
||||
@@ -118,7 +118,7 @@ const TriggerByDisplay: FC<TriggerByDisplayProps> = ({
|
||||
triggerMetadata,
|
||||
}) => {
|
||||
const { t } = useTranslation(['appLog'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
const displayName = getTriggerDisplayName(triggeredFrom, t, triggerMetadata)
|
||||
const icon = getTriggerIcon(triggeredFrom, triggerMetadata, theme)
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useLocale } from '#i18n'
|
||||
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
|
||||
import ThemeSelector from '@/app/components/base/theme-selector'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import ThemeSelector from '@/features/theme/theme-selector'
|
||||
import { setLocaleOnClient } from '@/i18n/client'
|
||||
import { languages } from '@/i18n/language'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
|
||||
@@ -3,8 +3,7 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
type AudioPlayerProps = Readonly<{
|
||||
src?: string // Keep backward compatibility
|
||||
@@ -22,7 +21,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
|
||||
const [hoverTime, setHoverTime] = useState(0)
|
||||
const [isAudioAvailable, setIsAudioAvailable] = useState(true)
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const playPauseLabel = t(($) => $[isPlaying ? 'operation.pause' : 'operation.play'], {
|
||||
ns: 'common',
|
||||
})
|
||||
@@ -214,10 +213,10 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
// Draw waveform bars
|
||||
data.forEach((value, index) => {
|
||||
let color
|
||||
if (index * barWidth <= playedWidth) color = theme === Theme.light ? '#296DFF' : '#84ABFF'
|
||||
if (index * barWidth <= playedWidth) color = theme === 'light' ? '#296DFF' : '#84ABFF'
|
||||
else if (((index * barWidth) / width) * duration <= hoverTime)
|
||||
color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
|
||||
else color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'
|
||||
color = theme === 'light' ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
|
||||
else color = theme === 'light' ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'
|
||||
const barHeight = value * height
|
||||
const rectX = index * barWidth
|
||||
const rectY = (height - barHeight) / 2
|
||||
@@ -295,7 +294,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="absolute top-0 left-0 flex size-full items-center justify-center text-text-quaternary"
|
||||
className="absolute inset-s-0 top-0 flex size-full items-center justify-center text-text-quaternary"
|
||||
hidden={isAudioAvailable}
|
||||
>
|
||||
{t(($) => $['operation.audioSourceUnavailable'], { ns: 'common' })}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { ResolvedTheme } from '@/features/theme/theme'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Theme } from '@/types/app'
|
||||
import { CodeBlock } from '../code-block'
|
||||
import { highlightCode } from '../code-highlight'
|
||||
|
||||
type UseThemeReturn = {
|
||||
theme: Theme
|
||||
theme: ResolvedTheme
|
||||
}
|
||||
|
||||
const mockUseTheme = vi.fn<() => UseThemeReturn>(() => ({ theme: Theme.light }))
|
||||
const mockUseTheme = vi.fn<() => UseThemeReturn>(() => ({ theme: 'light' }))
|
||||
const mockEcharts = vi.hoisted(() => {
|
||||
const state = {
|
||||
option: undefined as unknown,
|
||||
@@ -65,9 +65,9 @@ class MockAudioContext {
|
||||
}))
|
||||
}
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
vi.mock('@/features/theme/hooks', () => ({
|
||||
__esModule: true,
|
||||
default: () => mockUseTheme(),
|
||||
useResolvedTheme: () => mockUseTheme().theme,
|
||||
}))
|
||||
|
||||
// Keep the real highlighter so tests cover its output; the spy only forces
|
||||
@@ -149,7 +149,7 @@ const findHighlightedKeyword = async () => {
|
||||
describe('CodeBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseTheme.mockReturnValue({ theme: Theme.light })
|
||||
mockUseTheme.mockReturnValue({ theme: 'light' })
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
clientWidthSpy = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(900)
|
||||
@@ -249,7 +249,7 @@ describe('CodeBlock', () => {
|
||||
})
|
||||
|
||||
it('should render syntax-highlighted output when language is standard and app theme is dark', async () => {
|
||||
mockUseTheme.mockReturnValue({ theme: Theme.dark })
|
||||
mockUseTheme.mockReturnValue({ theme: 'dark' })
|
||||
|
||||
render(<CodeBlock className="language-javascript">const y = 2;</CodeBlock>)
|
||||
|
||||
@@ -361,7 +361,7 @@ describe('CodeBlock', () => {
|
||||
})
|
||||
|
||||
it('should render dark themed echarts path when app theme is dark', async () => {
|
||||
mockUseTheme.mockReturnValue({ theme: Theme.dark })
|
||||
mockUseTheme.mockReturnValue({ theme: 'dark' })
|
||||
render(<CodeBlock className="language-echarts">{'{"series":[]}'}</CodeBlock>)
|
||||
|
||||
expect(await findEchartsHost())!.toBeInTheDocument()
|
||||
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
} from 'react'
|
||||
import CopyIcon from '@/app/components/base/copy-icon'
|
||||
import ErrorBoundary from '@/app/components/base/markdown/error-boundary'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { highlightCode } from './code-highlight'
|
||||
|
||||
const MermaidDiagram = dynamic(
|
||||
@@ -156,7 +155,7 @@ type EChartsEventParams = {
|
||||
}
|
||||
|
||||
export const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const [isSVG, setIsSVG] = useState(true)
|
||||
const [chartState, setChartState] = useState<'loading' | 'success' | 'error'>('loading')
|
||||
const [finalChartOption, setFinalChartOption] = useState<EChartsOption | null>(null)
|
||||
@@ -171,7 +170,7 @@ export const CodeBlock: any = memo(({ inline, className, children = '', ...props
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const language = match?.[1]
|
||||
const languageShowName = getCorrectCapitalizationLanguageName(language || '')
|
||||
const isDarkMode = theme === Theme.dark
|
||||
const isDarkMode = theme === 'dark'
|
||||
|
||||
const clearResizeTimer = useCallback(() => {
|
||||
if (!resizeTimerRef.current) return
|
||||
@@ -555,7 +554,7 @@ export const CodeBlock: any = memo(({ inline, className, children = '', ...props
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex h-8 items-center justify-between rounded-t-[10px] border-b border-divider-subtle bg-components-input-bg-normal p-1 pl-3">
|
||||
<div className="flex h-8 items-center justify-between rounded-t-[10px] border-b border-divider-subtle bg-components-input-bg-normal p-1 ps-3">
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary">{languageShowName}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{language === 'svg' && (
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useDirection } from '@langgenius/dify-ui/direction-provider'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import {
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuRadioGroup,
|
||||
MenuRadioItem,
|
||||
MenuRadioItemIndicator,
|
||||
MenuTrigger,
|
||||
} from '@langgenius/dify-ui/menu'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const THEMES = ['light', 'dark', 'system'] as const
|
||||
export type Theme = (typeof THEMES)[number]
|
||||
|
||||
const isTheme = (value: string): value is Theme => {
|
||||
return (THEMES as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
const subscribeToNothing = () => () => {}
|
||||
|
||||
const useIsHydrated = () =>
|
||||
useSyncExternalStore(
|
||||
subscribeToNothing,
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
|
||||
export default function ThemeSelector() {
|
||||
const direction = useDirection()
|
||||
const { t } = useTranslation(['common'])
|
||||
const { theme, setTheme } = useTheme()
|
||||
const isHydrated = useIsHydrated()
|
||||
const currentTheme: Theme = theme && isTheme(theme) ? theme : 'system'
|
||||
|
||||
const getCurrentIcon = () => {
|
||||
if (!isHydrated) return <span aria-hidden className="size-4" />
|
||||
switch (currentTheme) {
|
||||
case 'light':
|
||||
return <span aria-hidden className="i-ri-sun-line size-4 text-text-tertiary" />
|
||||
case 'dark':
|
||||
return <span aria-hidden className="i-ri-moon-line size-4 text-text-tertiary" />
|
||||
default:
|
||||
return <span aria-hidden className="i-ri-computer-line size-4 text-text-tertiary" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<IconButton
|
||||
aria-label={t(($) => $['theme.theme'], { ns: 'common' })}
|
||||
size="lg"
|
||||
className="data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
{getCurrentIcon()}
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<MenuContent dir={direction} sideOffset={6} className="w-36">
|
||||
<MenuRadioGroup<Theme>
|
||||
value={currentTheme}
|
||||
onValueChange={(nextTheme) => setTheme(nextTheme)}
|
||||
>
|
||||
<MenuRadioItem<Theme> value="light" closeOnClick>
|
||||
<span className="i-ri-sun-line size-4 text-text-tertiary" />
|
||||
<span className="grow px-1 system-md-regular">
|
||||
{t(($) => $['theme.light'], { ns: 'common' })}
|
||||
</span>
|
||||
<MenuRadioItemIndicator data-testid="light-icon" />
|
||||
</MenuRadioItem>
|
||||
<MenuRadioItem<Theme> value="dark" closeOnClick>
|
||||
<span className="i-ri-moon-line size-4 text-text-tertiary" />
|
||||
<span className="grow px-1 system-md-regular">
|
||||
{t(($) => $['theme.dark'], { ns: 'common' })}
|
||||
</span>
|
||||
<MenuRadioItemIndicator data-testid="dark-icon" />
|
||||
</MenuRadioItem>
|
||||
<MenuRadioItem<Theme> value="system" closeOnClick>
|
||||
<span className="i-ri-computer-line size-4 text-text-tertiary" />
|
||||
<span className="grow px-1 system-md-regular">
|
||||
{t(($) => $['theme.auto'], { ns: 'common' })}
|
||||
</span>
|
||||
<MenuRadioItemIndicator data-testid="system-icon" />
|
||||
</MenuRadioItem>
|
||||
</MenuRadioGroup>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import type { SelfHostedPlan } from '../../../config'
|
||||
import { buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { SELF_HOSTED_PLAN_URLS } from '../../../config'
|
||||
import Community from '../../assets/community'
|
||||
import Enterprise from '../../assets/enterprise'
|
||||
@@ -41,7 +40,7 @@ const STYLE_MAP = {
|
||||
|
||||
export function SelfHostedPlanItem({ plan }: { plan: SelfHostedPlan }) {
|
||||
const { t } = useTranslation(['billing'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const i18nPrefix = `plans.${plan}` as const
|
||||
const isFreePlan = plan === 'community'
|
||||
const isPremiumPlan = plan === 'premium'
|
||||
@@ -79,7 +78,7 @@ export function SelfHostedPlanItem({ plan }: { plan: SelfHostedPlan }) {
|
||||
data-plan={plan}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'tertiary', size: null }),
|
||||
'h-auto min-h-12 w-full justify-start gap-x-2 rounded-none bg-components-button-tertiary-bg py-3 pr-4 pl-5 system-xl-semibold whitespace-normal text-text-primary hover:bg-components-button-tertiary-bg-hover data-[plan=enterprise]:bg-saas-dify-blue-static data-[plan=enterprise]:text-text-primary-on-surface data-[plan=enterprise]:hover:bg-saas-dify-blue-static-hover data-[plan=premium]:bg-saas-background-inverted data-[plan=premium]:py-2 data-[plan=premium]:text-background-default data-[plan=premium]:hover:bg-saas-background-inverted-hover xl:whitespace-nowrap',
|
||||
'h-auto min-h-12 w-full justify-start gap-x-2 rounded-none bg-components-button-tertiary-bg py-3 ps-5 pe-4 system-xl-semibold whitespace-normal text-text-primary hover:bg-components-button-tertiary-bg-hover data-[plan=enterprise]:bg-saas-dify-blue-static data-[plan=enterprise]:text-text-primary-on-surface data-[plan=enterprise]:hover:bg-saas-dify-blue-static-hover data-[plan=premium]:bg-saas-background-inverted data-[plan=premium]:py-2 data-[plan=premium]:text-background-default data-[plan=premium]:hover:bg-saas-background-inverted-hover xl:whitespace-nowrap',
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 grow flex-wrap items-center gap-x-2 xl:flex-nowrap">
|
||||
@@ -92,7 +91,7 @@ export function SelfHostedPlanItem({ plan }: { plan: SelfHostedPlan }) {
|
||||
)}
|
||||
{isPremiumPlan && (
|
||||
<span aria-hidden className="pt-1.75 pb-px">
|
||||
{theme === Theme.light ? (
|
||||
{theme === 'light' ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-custom-public-billing-aws-marketplace-light h-6 w-31.5"
|
||||
|
||||
@@ -6,8 +6,7 @@ import dynamic from 'next/dynamic'
|
||||
import { useId, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { formatFileSize, getFileExtension } from '@/utils/format'
|
||||
import { PROGRESS_COMPLETE, PROGRESS_ERROR } from '../constants'
|
||||
|
||||
@@ -26,8 +25,8 @@ const FileListItem = ({ fileItem, onPreview, onRemove }: FileListItemProps) => {
|
||||
const fileNameId = useId()
|
||||
const previewLabelId = useId()
|
||||
const removeLabelId = useId()
|
||||
const { theme } = useTheme()
|
||||
const chartColor = useMemo(() => (theme === Theme.dark ? '#5289ff' : '#296dff'), [theme])
|
||||
const theme = useResolvedTheme()
|
||||
const chartColor = useMemo(() => (theme === 'dark' ? '#5289ff' : '#296dff'), [theme])
|
||||
|
||||
const isUploading = fileItem.progress >= 0 && fileItem.progress < PROGRESS_COMPLETE
|
||||
const isError = fileItem.progress === PROGRESS_ERROR
|
||||
@@ -43,7 +42,7 @@ const FileListItem = ({ fileItem, onPreview, onRemove }: FileListItemProps) => {
|
||||
disabled={!fileItem.file?.id}
|
||||
aria-labelledby={`${previewLabelId} ${fileNameId}`}
|
||||
onClick={handleClick}
|
||||
className="flex h-full min-w-0 grow items-center rounded-lg text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
className="flex h-full min-w-0 grow items-center rounded-lg text-start focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
>
|
||||
<span id={previewLabelId} className="sr-only">
|
||||
{t(($) => $['stepOne.filePreview'], { ns: 'datasetCreation' })}
|
||||
@@ -69,7 +68,7 @@ const FileListItem = ({ fileItem, onPreview, onRemove }: FileListItemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex w-16 shrink-0 items-center justify-end gap-1 pr-3">
|
||||
<div className="flex w-16 shrink-0 items-center justify-end gap-1 pe-3">
|
||||
{isUploading && (
|
||||
<SimplePieChart
|
||||
percentage={fileItem.progress}
|
||||
|
||||
+4
-5
@@ -5,8 +5,7 @@ import dynamic from 'next/dynamic'
|
||||
import { useMemo } from 'react'
|
||||
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
|
||||
import { getFileType } from '@/app/components/datasets/common/image-uploader/utils'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import { PROGRESS_ERROR } from '../constants'
|
||||
|
||||
@@ -21,8 +20,8 @@ type FileListItemProps = {
|
||||
}
|
||||
|
||||
const FileListItem = ({ fileItem, onPreview, onRemove }: FileListItemProps) => {
|
||||
const { theme } = useTheme()
|
||||
const chartColor = useMemo(() => (theme === Theme.dark ? '#5289ff' : '#296dff'), [theme])
|
||||
const theme = useResolvedTheme()
|
||||
const chartColor = useMemo(() => (theme === 'dark' ? '#5289ff' : '#296dff'), [theme])
|
||||
|
||||
const isUploading = fileItem.progress >= 0 && fileItem.progress < 100
|
||||
const isError = fileItem.progress === PROGRESS_ERROR
|
||||
@@ -62,7 +61,7 @@ const FileListItem = ({ fileItem, onPreview, onRemove }: FileListItemProps) => {
|
||||
<span>{formatFileSize(fileItem.file.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-16 shrink-0 items-center justify-end gap-1 pr-3">
|
||||
<div className="flex w-16 shrink-0 items-center justify-end gap-1 pe-3">
|
||||
{isUploading && (
|
||||
<SimplePieChart
|
||||
percentage={fileItem.progress}
|
||||
|
||||
@@ -11,10 +11,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
||||
import { toast } from '@/app/notifications'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { upload } from '@/service/base'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
type Props = Readonly<{
|
||||
file: FileItem | undefined
|
||||
@@ -175,8 +174,8 @@ const CSVUploader: FC<Props> = ({ file, updateFile }) => {
|
||||
if (!isValid(currentFile)) return
|
||||
initialUpload(currentFile)
|
||||
}
|
||||
const { theme } = useTheme()
|
||||
const chartColor = useMemo(() => (theme === Theme.dark ? '#5289ff' : '#296dff'), [theme])
|
||||
const theme = useResolvedTheme()
|
||||
const chartColor = useMemo(() => (theme === 'dark' ? '#5289ff' : '#296dff'), [theme])
|
||||
useEffect(() => {
|
||||
dropRef.current?.addEventListener('dragenter', handleDragEnter)
|
||||
dropRef.current?.addEventListener('dragover', handleDragOver)
|
||||
@@ -213,14 +212,14 @@ const CSVUploader: FC<Props> = ({ file, updateFile }) => {
|
||||
{t(($) => $['list.batchModal.csvUploadTitle'], { ns: 'datasetDocuments' })}
|
||||
<button
|
||||
type="button"
|
||||
className="inline cursor-pointer border-none bg-transparent p-0 text-left text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="inline cursor-pointer border-none bg-transparent p-0 text-start text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={selectHandle}
|
||||
>
|
||||
{t(($) => $['list.batchModal.browse'], { ns: 'datasetDocuments' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className="absolute top-0 left-0 size-full" />}
|
||||
{dragging && <div ref={dragRef} className="absolute inset-s-0 top-0 size-full" />}
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
@@ -231,7 +230,7 @@ const CSVUploader: FC<Props> = ({ file, updateFile }) => {
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="i-custom-public-files-csv h-8.5 w-8 shrink-0" />
|
||||
<div className="ml-2 flex w-0 grow">
|
||||
<div className="ms-2 flex w-0 grow">
|
||||
<span className="max-w-[calc(100%-30px)] overflow-hidden text-ellipsis whitespace-nowrap text-text-primary">
|
||||
{file.file.name.replace(/.csv$/, '')}
|
||||
</span>
|
||||
|
||||
@@ -7,13 +7,13 @@ import { zSkillResponse } from '@dify/contracts/api/console/workspaces/zod.gen'
|
||||
import { detectPlatform, useHotkey } from '@tanstack/react-hotkeys'
|
||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { createNode } from '@/app/components/workflow/__tests__/fixtures'
|
||||
import { renderWorkflowFlowHook } from '@/app/components/workflow/__tests__/workflow-test-env'
|
||||
import { useWorkflowSearch } from '@/app/components/workflow/hooks/use-workflow-search'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { AppToaster } from '@/app/notifications/app-toaster'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { seedCurrentWorkspaceQuery } from '@/test/console/current-workspace'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { createDeferred } from '@/test/deferred'
|
||||
@@ -129,7 +129,7 @@ const plugin: MarketplacePlugin = {
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
}
|
||||
const darkTheme = /app.gotoAnything.actions.themeDark/
|
||||
const darkTheme = /common.theme.dark/
|
||||
const isMac = detectPlatform() === 'mac'
|
||||
const triggerSearchShortcut = (target: HTMLElement = document.body) =>
|
||||
fireEvent.keyDown(target, { key: 'k', ctrlKey: !isMac, metaKey: isMac })
|
||||
@@ -151,7 +151,7 @@ function setup({ datasetOperator = false, enableSkill = true, canInstall = true
|
||||
})
|
||||
render(
|
||||
<ConsoleWrapper>
|
||||
<ThemeProvider defaultTheme="light" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="light">
|
||||
<GotoAnything />
|
||||
<AppToaster timeout={0} />
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { ReactNode } from 'react'
|
||||
import type { consoleClient } from '@/service/console'
|
||||
import type { marketplaceClient } from '@/service/marketplace'
|
||||
import { detectPlatform } from '@tanstack/react-hotkeys'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { page, userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { MainNavSearchButton } from '@/app/components/main-nav/components/search-button'
|
||||
import { MainNavMobileBar } from '@/app/components/main-nav/mobile-nav'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { createDatasetFixture } from '@/test/fixtures/dataset'
|
||||
import { createPluginDeclarationFixture } from '@/test/fixtures/plugin'
|
||||
@@ -118,7 +118,7 @@ function renderPalette(trigger: ReactNode = <MainNavSearchButton />) {
|
||||
})
|
||||
return render(
|
||||
<ConsoleWrapper>
|
||||
<ThemeProvider defaultTheme="light" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="light">
|
||||
{trigger}
|
||||
<GotoAnything />
|
||||
</ThemeProvider>
|
||||
@@ -268,7 +268,7 @@ describe('desktop dialog', () => {
|
||||
await page.getByRole('option', { name: theme }).click()
|
||||
await expect.element(searchField(theme)).toHaveFocus()
|
||||
await expect
|
||||
.element(page.getByRole('option', { name: /app.gotoAnything.actions.themeDark/ }))
|
||||
.element(page.getByRole('option', { name: /common.theme.dark/ }))
|
||||
.toBeInTheDocument()
|
||||
|
||||
await userEvent.keyboard('{Backspace}')
|
||||
|
||||
@@ -24,12 +24,12 @@ describe('command journeys', () => {
|
||||
it('lists and filters themes, and activates the selected theme', () => {
|
||||
const context = createCommandContext()
|
||||
expect(slashCommandRegistry.search('/theme', context).map((result) => result.id)).toEqual([
|
||||
'system',
|
||||
'light',
|
||||
'dark',
|
||||
'system',
|
||||
])
|
||||
executeResult('/theme dark', context)
|
||||
expect(context.setTheme).toHaveBeenCalledExactlyOnceWith('dark')
|
||||
expect(context.setThemePreference).toHaveBeenCalledExactlyOnceWith('dark')
|
||||
})
|
||||
|
||||
it('finds a language by its native name and activates it through the alias', async () => {
|
||||
@@ -109,8 +109,8 @@ describe('command journeys', () => {
|
||||
executeResult('/theme dark', second)
|
||||
expect(first.openExternal).toHaveBeenCalledExactlyOnceWith('https://one.example/docs')
|
||||
expect(second.openExternal).toHaveBeenCalledExactlyOnceWith('https://two.example/docs')
|
||||
expect(first.setTheme).not.toHaveBeenCalled()
|
||||
expect(second.setTheme).toHaveBeenCalledExactlyOnceWith('dark')
|
||||
expect(first.setThemePreference).not.toHaveBeenCalled()
|
||||
expect(second.setThemePreference).toHaveBeenCalledExactlyOnceWith('dark')
|
||||
})
|
||||
|
||||
it('renders localized commands on the server without a browser or activation effects', async () => {
|
||||
@@ -118,8 +118,8 @@ describe('command journeys', () => {
|
||||
await i18n.init({
|
||||
lng: 'en-US',
|
||||
resources: {
|
||||
'en-US': { app: { 'gotoAnything.actions.themeDark': 'Dark' } },
|
||||
'zh-Hans': { app: { 'gotoAnything.actions.themeDark': '深色' } },
|
||||
'en-US': { common: { 'theme.dark': 'Dark' } },
|
||||
'zh-Hans': { common: { 'theme.dark': '深色' } },
|
||||
},
|
||||
})
|
||||
const first = createCommandContext({ t: i18n.t as CommandContext['t'] })
|
||||
@@ -143,7 +143,7 @@ describe('command journeys', () => {
|
||||
for (const context of [first, second]) {
|
||||
slashCommandRegistry.search('/', context)
|
||||
languageCommand.search('', context)
|
||||
expect(context.setTheme).not.toHaveBeenCalled()
|
||||
expect(context.setThemePreference).not.toHaveBeenCalled()
|
||||
expect(context.openExternal).not.toHaveBeenCalled()
|
||||
expect(context.navigate).not.toHaveBeenCalled()
|
||||
expect(context.openGenerator).not.toHaveBeenCalled()
|
||||
|
||||
@@ -8,7 +8,7 @@ export function createCommandContext(overrides: Partial<CommandContext> = {}): C
|
||||
agentsAvailable: true,
|
||||
skillsAvailable: true,
|
||||
currentApp: null,
|
||||
setTheme: vi.fn(),
|
||||
setThemePreference: vi.fn(),
|
||||
setLocale: vi.fn().mockResolvedValue(undefined),
|
||||
getDocsHomeUrl: () => 'https://docs.dify.ai/en',
|
||||
onError: vi.fn(),
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
import type { CommandSearchResult } from '../types'
|
||||
import type { CommandContext, SlashCommand } from './types'
|
||||
import type { ThemePreference } from '@/features/theme/theme'
|
||||
import { isThemePreference, themeOptions } from '@/features/theme/theme'
|
||||
|
||||
const THEME_ITEMS = [
|
||||
{
|
||||
id: 'system',
|
||||
titleKey: 'gotoAnything.actions.themeSystem',
|
||||
descKey: 'gotoAnything.actions.themeSystemDesc',
|
||||
iconClassName: 'i-ri-computer-line',
|
||||
},
|
||||
{
|
||||
id: 'light',
|
||||
titleKey: 'gotoAnything.actions.themeLight',
|
||||
descKey: 'gotoAnything.actions.themeLightDesc',
|
||||
iconClassName: 'i-ri-sun-line',
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
titleKey: 'gotoAnything.actions.themeDark',
|
||||
descKey: 'gotoAnything.actions.themeDarkDesc',
|
||||
iconClassName: 'i-ri-moon-line',
|
||||
},
|
||||
] as const
|
||||
const themeCommandDescriptionKeys = {
|
||||
light: 'gotoAnything.actions.themeLightDesc',
|
||||
dark: 'gotoAnything.actions.themeDarkDesc',
|
||||
system: 'gotoAnything.actions.themeSystemDesc',
|
||||
} as const satisfies Record<ThemePreference, string>
|
||||
|
||||
const buildThemeCommands = (query: string, context: CommandContext): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = THEME_ITEMS.filter(
|
||||
(item) =>
|
||||
!q ||
|
||||
context
|
||||
.t(($) => $[item.titleKey], { ns: 'app', lng: context.locale })
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
item.id.includes(q),
|
||||
)
|
||||
return list.map((item) => ({
|
||||
id: item.id,
|
||||
title: context.t(($) => $[item.titleKey], { ns: 'app', lng: context.locale }),
|
||||
description: context.t(($) => $[item.descKey], { ns: 'app', lng: context.locale }),
|
||||
type: 'command' as const,
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg">
|
||||
<span aria-hidden className={`${item.iconClassName} size-4 text-text-tertiary`} />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'theme', args: { value: item.id } },
|
||||
}))
|
||||
return themeOptions
|
||||
.map((option) => ({
|
||||
...option,
|
||||
title: context.t(($) => $[option.labelKey], { ns: 'common', lng: context.locale }),
|
||||
}))
|
||||
.filter((option) => !q || option.title.toLowerCase().includes(q) || option.value.includes(q))
|
||||
.map((option) => ({
|
||||
id: option.value,
|
||||
title: option.title,
|
||||
description: context.t(($) => $[themeCommandDescriptionKeys[option.value]], {
|
||||
ns: 'app',
|
||||
lng: context.locale,
|
||||
}),
|
||||
type: 'command' as const,
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg">
|
||||
<span aria-hidden className={`${option.iconClassName} size-4 text-text-tertiary`} />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'theme', args: { value: option.value } },
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,11 +39,10 @@ const buildThemeCommands = (query: string, context: CommandContext): CommandSear
|
||||
*/
|
||||
export const themeCommand: SlashCommand = {
|
||||
name: 'theme',
|
||||
description: 'Switch between light and dark themes',
|
||||
description: 'Change appearance',
|
||||
mode: 'submenu',
|
||||
execute(args, context) {
|
||||
const item = THEME_ITEMS.find((item) => item.id === args.value)
|
||||
if (item) context.setTheme(item.id)
|
||||
if (isThemePreference(args.value)) context.setThemePreference(args.value)
|
||||
},
|
||||
|
||||
search(args: string, context) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { CommandSearchResult } from '../types'
|
||||
import type { useWorkflowGeneratorStore } from '@/app/components/workflow/workflow-generator/store'
|
||||
import type { ThemePreference } from '@/features/theme/theme'
|
||||
|
||||
export type CommandContext = {
|
||||
t: TFunction<['app', 'common', 'modelProvider', 'accountSettings']>
|
||||
@@ -8,7 +9,7 @@ export type CommandContext = {
|
||||
agentsAvailable: boolean
|
||||
skillsAvailable: boolean
|
||||
currentApp: { id: string; mode: 'workflow' | 'advanced-chat' } | null
|
||||
setTheme: (theme: string) => void
|
||||
setThemePreference: (preference: ThemePreference) => void
|
||||
setLocale: (locale: string) => Promise<void>
|
||||
getDocsHomeUrl: () => string
|
||||
onError: (error: unknown) => void
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CommandContext } from './types'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflowGeneratorStore } from '@/app/components/workflow/workflow-generator/store'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useThemePreference } from '@/features/theme/hooks'
|
||||
import { setLocaleOnClient } from '@/i18n/client'
|
||||
import { useParams, useRouter } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
@@ -22,7 +22,7 @@ export function useCommandContext(
|
||||
isWorkflowPage: boolean,
|
||||
): CommandContext {
|
||||
const { t, i18n } = useTranslation(['app', 'common', 'modelProvider', 'accountSettings'])
|
||||
const { setTheme } = useTheme()
|
||||
const { setPreference: setThemePreference } = useThemePreference()
|
||||
const { push, refresh } = useRouter()
|
||||
const getDocsHomeUrl = useDocLink()
|
||||
const { appId: routeAppId } = useParams<{ appId?: string }>()
|
||||
@@ -47,7 +47,7 @@ export function useCommandContext(
|
||||
isWorkflowPage && appId && (appMode === 'workflow' || appMode === 'advanced-chat')
|
||||
? { id: appId, mode: appMode }
|
||||
: null,
|
||||
setTheme,
|
||||
setThemePreference,
|
||||
setLocale: async (locale) => {
|
||||
await setLocaleOnClient(locale)
|
||||
refresh()
|
||||
@@ -69,7 +69,7 @@ export function useCommandContext(
|
||||
isWorkflowPage,
|
||||
appId,
|
||||
appMode,
|
||||
setTheme,
|
||||
setThemePreference,
|
||||
getDocsHomeUrl,
|
||||
push,
|
||||
refresh,
|
||||
|
||||
@@ -79,10 +79,10 @@ it.each(['ltr', 'rtl'] as const)(
|
||||
)
|
||||
await screen.getByRole('button', { name: 'accountSettings.account.account' }).click()
|
||||
const appearance = screen.getByRole('menuitem', {
|
||||
name: 'accountSettings.account.appearanceLabel',
|
||||
name: 'common.theme.label',
|
||||
})
|
||||
await userEvent.hover(appearance)
|
||||
const submenu = screen.getByRole('menu', { name: 'accountSettings.account.appearanceLabel' })
|
||||
const submenu = screen.getByRole('menu', { name: 'common.theme.label' })
|
||||
await expect.element(submenu).toBeVisible()
|
||||
// Leave room on both sides so collision fallback cannot hide a fixed-side override.
|
||||
await expect
|
||||
@@ -94,9 +94,7 @@ it.each(['ltr', 'rtl'] as const)(
|
||||
: submenuBounds.left >= triggerBounds.right
|
||||
})
|
||||
.toBe(true)
|
||||
await userEvent.hover(
|
||||
screen.getByRole('menuitemradio', { name: 'accountSettings.account.appearanceLight' }),
|
||||
)
|
||||
await userEvent.hover(screen.getByRole('menuitemradio', { name: 'common.theme.light' }))
|
||||
await expect.element(submenu).toBeVisible()
|
||||
await userEvent.keyboard('{Escape}')
|
||||
await expect.element(appearance).toHaveFocus()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import type { Theme } from '@/app/components/base/theme-selector'
|
||||
import type { ThemePreference } from '@/features/theme/theme'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@langgenius/dify-ui/avatar'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
MenuTrigger,
|
||||
} from '@langgenius/dify-ui/menu'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useRef, useSyncExternalStore } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -29,6 +28,8 @@ import {
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useThemePreference } from '@/features/theme/hooks'
|
||||
import { themeOptions } from '@/features/theme/theme'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
@@ -70,42 +71,31 @@ function MainNavRadioItemContent({ iconClassName, label }: MainNavRadioItemConte
|
||||
}
|
||||
|
||||
function AppearanceSubmenu() {
|
||||
const { t } = useTranslation(['accountSettings'])
|
||||
const { theme, setTheme } = useTheme()
|
||||
const currentTheme: Theme =
|
||||
theme === 'light' || theme === 'dark' || theme === 'system' ? theme : 'system'
|
||||
const { t } = useTranslation(['common'])
|
||||
const { preference, setPreference } = useThemePreference()
|
||||
|
||||
return (
|
||||
<MenuSubmenu>
|
||||
<MenuSubmenuTrigger className="px-3 py-1">
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-sun-line"
|
||||
label={t(($) => $['account.appearanceLabel'], { ns: 'accountSettings' })}
|
||||
label={t(($) => $['theme.label'], { ns: 'common' })}
|
||||
/>
|
||||
</MenuSubmenuTrigger>
|
||||
<MenuContent className="max-h-90 w-34.75">
|
||||
<MenuRadioGroup<Theme>
|
||||
value={currentTheme}
|
||||
onValueChange={(nextTheme) => setTheme(nextTheme)}
|
||||
>
|
||||
<MenuRadioItem<Theme> value="light" className="py-1">
|
||||
<MainNavRadioItemContent
|
||||
iconClassName="i-ri-sun-line"
|
||||
label={t(($) => $['account.appearanceLight'], { ns: 'accountSettings' })}
|
||||
/>
|
||||
</MenuRadioItem>
|
||||
<MenuRadioItem<Theme> value="dark" className="py-1">
|
||||
<MainNavRadioItemContent
|
||||
iconClassName="i-ri-moon-line"
|
||||
label={t(($) => $['account.appearanceDark'], { ns: 'accountSettings' })}
|
||||
/>
|
||||
</MenuRadioItem>
|
||||
<MenuRadioItem<Theme> value="system" className="py-1">
|
||||
<MainNavRadioItemContent
|
||||
iconClassName="i-ri-computer-line"
|
||||
label={t(($) => $['account.appearanceSystem'], { ns: 'accountSettings' })}
|
||||
/>
|
||||
</MenuRadioItem>
|
||||
<MenuRadioGroup<ThemePreference> value={preference} onValueChange={setPreference}>
|
||||
{themeOptions.map((option) => (
|
||||
<MenuRadioItem<ThemePreference>
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className="py-1"
|
||||
>
|
||||
<MainNavRadioItemContent
|
||||
iconClassName={option.iconClassName}
|
||||
label={t(($) => $[option.labelKey], { ns: 'common' })}
|
||||
/>
|
||||
</MenuRadioItem>
|
||||
))}
|
||||
</MenuRadioGroup>
|
||||
</MenuContent>
|
||||
</MenuSubmenu>
|
||||
|
||||
@@ -143,7 +143,7 @@ it('opens preferences from the URL control, switches real pages, and closes with
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'Open preferences' }))
|
||||
const dialog = await findSettings()
|
||||
expect(within(dialog).getByText('accountSettings.account.appearanceLabel')).toBeInTheDocument()
|
||||
expect(within(dialog).getByText('common.theme.label')).toBeInTheDocument()
|
||||
expect(within(dialog).getByRole('button', { current: 'page' })).toHaveAccessibleName(
|
||||
'navigation.settings.preferences',
|
||||
)
|
||||
@@ -312,7 +312,7 @@ it.each([
|
||||
])('hides billing and falls back to preferences for $reason', async ({ options }) => {
|
||||
renderSettings(TAB.BILLING, options)
|
||||
const dialog = await findSettings()
|
||||
expect(within(dialog).getByText('accountSettings.account.appearanceLabel')).toBeInTheDocument()
|
||||
expect(within(dialog).getByText('common.theme.label')).toBeInTheDocument()
|
||||
expect(
|
||||
within(dialog).queryByRole('button', { name: 'navigation.settings.billing' }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
+2
-2
@@ -7,12 +7,12 @@ import type { ConsoleQueryTestOptions } from '@/test/console/query-data'
|
||||
import { zPluginCategoryInstalledPluginResponse } from '@dify/contracts/api/console/workspaces/zod.gen'
|
||||
import { act, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { AppToaster } from '@/app/notifications/app-toaster'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
@@ -128,7 +128,7 @@ function setup(
|
||||
const queryClient = createConsoleQueryClient()
|
||||
seedModelProviderQueries(queryClient)
|
||||
renderWithConsoleQuery(
|
||||
<ThemeProvider defaultTheme="light" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="light">
|
||||
<NuqsTestingAdapter>
|
||||
<DataSourcePage {...props} />
|
||||
<ReadmePanel />
|
||||
|
||||
+2
-2
@@ -3,7 +3,6 @@ import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { MarketplaceLinks } from '@/features/marketplace/links'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
@@ -11,6 +10,7 @@ import List from '@/app/components/plugins/marketplace/list'
|
||||
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { useMarketplaceAllPlugins } from './hooks'
|
||||
|
||||
@@ -27,7 +27,7 @@ const InstallFromMarketplace = ({
|
||||
searchText,
|
||||
}: InstallFromMarketplaceProps) => {
|
||||
const { t } = useTranslation(['plugin', 'modelProvider'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { canInstallPlugin } = usePluginSettingsAccess()
|
||||
const [collapse, setCollapse] = useState(false)
|
||||
const { plugins: allPlugins, isLoading: isAllPluginsLoading } = useMarketplaceAllPlugins(
|
||||
|
||||
+2
-2
@@ -3,7 +3,6 @@ import type { MarketplaceLinks } from '@/features/marketplace/links'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
@@ -11,6 +10,7 @@ import List from '@/app/components/plugins/marketplace/list'
|
||||
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useMarketplaceAllPlugins } from './hooks'
|
||||
@@ -28,7 +28,7 @@ const InstallFromMarketplace = ({
|
||||
stepByStepTourTarget,
|
||||
}: InstallFromMarketplaceProps) => {
|
||||
const { t } = useTranslation(['plugin', 'modelProvider'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { canInstallPlugin } = usePluginSettingsAccess()
|
||||
const [collapse, setCollapse] = useState(false)
|
||||
const [hasEnteredViewport, setHasEnteredViewport] = useState(
|
||||
|
||||
@@ -6,9 +6,8 @@ import type { FC } from 'react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import type { ModelSelectorProvider } from '../model-selector/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { renderI18nObject } from '@/i18n/metadata'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useLanguage } from '../hooks'
|
||||
|
||||
type ModelIconProps = {
|
||||
@@ -29,13 +28,13 @@ const ModelIcon: FC<ModelIconProps> = ({
|
||||
iconClassName,
|
||||
isDeprecated = false,
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const language = useLanguage()
|
||||
const lightIconUrl = provider?.icon_small ? renderI18nObject(provider.icon_small, language) : ''
|
||||
const darkIconUrl = provider?.icon_small_dark
|
||||
? renderI18nObject(provider.icon_small_dark, language)
|
||||
: ''
|
||||
const iconUrl = theme === Theme.dark ? darkIconUrl || lightIconUrl : lightIconUrl
|
||||
const iconUrl = theme === 'dark' ? darkIconUrl || lightIconUrl : lightIconUrl
|
||||
|
||||
if (
|
||||
provider?.provider &&
|
||||
|
||||
+1
-1
@@ -524,7 +524,7 @@ describe('workspace credits and marketplace availability', () => {
|
||||
expect(screen.queryByRole('button', { name: /selector.install/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /discoverMoreInMarketplace/ })).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringMatching(/\/plugins\/model$/),
|
||||
expect.stringMatching(/\/plugins\/model\?theme=light$/),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+2
-2
@@ -6,6 +6,7 @@ import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/d
|
||||
import { useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { modelNameMap, providerLogoMap } from '../utils'
|
||||
|
||||
type MarketplaceSectionProps = {
|
||||
@@ -14,7 +15,6 @@ type MarketplaceSectionProps = {
|
||||
marketplaceCollapsed: boolean
|
||||
installingProvider: ModelProviderQuotaGetPaid | null
|
||||
canInstallPlugin: boolean
|
||||
theme?: string
|
||||
onMarketplaceCollapsedChange: (collapsed: boolean) => void
|
||||
onInstallPlugin: (key: ModelProviderQuotaGetPaid) => void | Promise<void>
|
||||
onOpenMarketplace?: () => void
|
||||
@@ -26,12 +26,12 @@ function MarketplaceSection({
|
||||
marketplaceCollapsed,
|
||||
installingProvider,
|
||||
canInstallPlugin,
|
||||
theme,
|
||||
onMarketplaceCollapsedChange,
|
||||
onInstallPlugin,
|
||||
onOpenMarketplace,
|
||||
}: MarketplaceSectionProps) {
|
||||
const { t } = useTranslation(['plugin', 'modelProvider'])
|
||||
const theme = useResolvedTheme()
|
||||
const headingId = useId()
|
||||
|
||||
if (marketplaceProviders.length === 0) return null
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
PreviewCardContent,
|
||||
} from '@langgenius/dify-ui/preview-card'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
@@ -84,7 +83,6 @@ function Popup({
|
||||
onHide,
|
||||
}: PopupProps) {
|
||||
const { t } = useTranslation(['common', 'modelProvider'])
|
||||
const { theme } = useTheme()
|
||||
const language = useLanguage()
|
||||
const [previewCardHandle] = useState(() => createPreviewCardHandle<ModelSelectorPreviewPayload>())
|
||||
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
|
||||
@@ -313,7 +311,6 @@ function Popup({
|
||||
marketplaceCollapsed={marketplaceCollapsed}
|
||||
installingProvider={installingProvider}
|
||||
canInstallPlugin={canInstallPlugin}
|
||||
theme={theme}
|
||||
onMarketplaceCollapsedChange={setMarketplaceCollapsed}
|
||||
onInstallPlugin={handleInstallPlugin}
|
||||
onOpenMarketplace={onOpenMarketplace}
|
||||
|
||||
+5
-6
@@ -1,9 +1,8 @@
|
||||
import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { renderI18nObject } from '@/i18n/metadata'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useLanguage } from '../hooks'
|
||||
|
||||
type ProviderIconProps = {
|
||||
@@ -14,21 +13,21 @@ type ProviderIconProps = {
|
||||
className?: string
|
||||
}
|
||||
const ProviderIcon: FC<ProviderIconProps> = ({ provider, className }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const language = useLanguage()
|
||||
const lightIconUrl = provider.icon_small ? renderI18nObject(provider.icon_small, language) : ''
|
||||
const darkIconUrl = provider.icon_small_dark
|
||||
? renderI18nObject(provider.icon_small_dark, language)
|
||||
: ''
|
||||
const iconUrl = theme === Theme.dark ? darkIconUrl || lightIconUrl : lightIconUrl
|
||||
const iconUrl = theme === 'dark' ? darkIconUrl || lightIconUrl : lightIconUrl
|
||||
|
||||
if (provider.provider === 'langgenius/anthropic/anthropic') {
|
||||
return (
|
||||
<div className={cn('py-1.75', className)}>
|
||||
{theme === Theme.dark && (
|
||||
{theme === 'dark' && (
|
||||
<span aria-hidden className="i-custom-public-llm-anthropic-light h-2.5 w-22.5" />
|
||||
)}
|
||||
{theme === Theme.light && (
|
||||
{theme === 'light' && (
|
||||
<span aria-hidden className="i-custom-public-llm-anthropic-dark h-2.5 w-22.5" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('PreferencePage - Interactions', () => {
|
||||
const language = screen.getByRole('combobox', { name: 'common.language.displayLanguage' })
|
||||
const timezone = screen.getByRole('combobox', { name: 'common.language.timezone' })
|
||||
const appearance = screen.getByRole('combobox', {
|
||||
name: 'accountSettings.account.appearanceLabel',
|
||||
name: 'common.theme.label',
|
||||
})
|
||||
|
||||
await selectOption('common.language.displayLanguage', chinese.name)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import type { ThemePreference } from '@/features/theme/theme'
|
||||
import type { Locale } from '@/i18n'
|
||||
import {
|
||||
Combobox,
|
||||
@@ -30,12 +31,13 @@ import {
|
||||
SelectValue,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocale } from '#i18n'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useThemePreference } from '@/features/theme/hooks'
|
||||
import { themeOptions } from '@/features/theme/theme'
|
||||
import { setLocaleOnClient } from '@/i18n/client'
|
||||
import { languages } from '@/i18n/language'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
@@ -44,7 +46,6 @@ import { timezones } from '@/utils/timezone'
|
||||
|
||||
type TimezoneOption = (typeof timezones)[number]
|
||||
type TimezoneValue = TimezoneOption['value']
|
||||
type ThemeValue = 'system' | 'light' | 'dark'
|
||||
|
||||
const languageItems = languages
|
||||
.filter((item) => item.supported)
|
||||
@@ -67,7 +68,7 @@ export default function PreferencePage() {
|
||||
})
|
||||
const { t } = useTranslation(['common', 'accountSettings'])
|
||||
const router = useRouter()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const { preference, setPreference } = useThemePreference()
|
||||
const updateProfile = useMutation(
|
||||
consoleQuery.account.profile.patch.mutationOptions({
|
||||
onSuccess: async (data, variables, onMutateResult, context) => {
|
||||
@@ -90,39 +91,32 @@ export default function PreferencePage() {
|
||||
},
|
||||
}),
|
||||
)
|
||||
const themeItems = [
|
||||
{
|
||||
value: 'system',
|
||||
label: t(($) => $['account.appearanceFollowSystem'], { ns: 'accountSettings' }),
|
||||
},
|
||||
{ value: 'light', label: t(($) => $['account.appearanceLight'], { ns: 'accountSettings' }) },
|
||||
{ value: 'dark', label: t(($) => $['account.appearanceDark'], { ns: 'accountSettings' }) },
|
||||
] satisfies { value: ThemeValue; label: string }[]
|
||||
const themeItems = themeOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: t(($) => $[option.labelKey], { ns: 'common' }),
|
||||
}))
|
||||
const selectedLanguage = languageItems.find(
|
||||
(item) => item.value === (locale || userProfile.interface_language),
|
||||
)
|
||||
const selectedTheme = themeItems.find((item) => item.value === theme)?.value ?? 'system'
|
||||
const selectedTimezone = timezones.find((item) => item.value === userProfile.timezone)
|
||||
return (
|
||||
<div className="grid gap-6 pt-1 pb-4">
|
||||
<div className="grid min-w-0 gap-1">
|
||||
<Select<ThemeValue>
|
||||
<Select<ThemePreference>
|
||||
items={themeItems}
|
||||
value={selectedTheme}
|
||||
value={preference}
|
||||
onValueChange={(nextValue) => {
|
||||
if (nextValue === null || nextValue === selectedTheme) return
|
||||
setTheme(nextValue)
|
||||
if (nextValue === null || nextValue === preference) return
|
||||
setPreference(nextValue)
|
||||
}}
|
||||
>
|
||||
<SelectLabel>
|
||||
{t(($) => $['account.appearanceLabel'], { ns: 'accountSettings' })}
|
||||
</SelectLabel>
|
||||
<SelectLabel>{t(($) => $['theme.label'], { ns: 'common' })}</SelectLabel>
|
||||
<SelectTrigger size="medium">
|
||||
<SelectValue<ThemeValue> />
|
||||
<SelectValue<ThemePreference> />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{themeItems.map((item) => (
|
||||
<SelectItem<ThemeValue> key={item.value} value={item.value}>
|
||||
<SelectItem<ThemePreference> key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.label}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
import { Theme } from '@/types/app'
|
||||
import IconWithTooltip from '../icon-with-tooltip'
|
||||
|
||||
describe('IconWithTooltip', () => {
|
||||
it('exposes the badge meaning without opening the tooltip', () => {
|
||||
render(
|
||||
<IconWithTooltip
|
||||
theme={Theme.light}
|
||||
theme="light"
|
||||
lightIconClassName="i-custom-public-plugins-partner-light"
|
||||
darkIconClassName="i-custom-public-plugins-partner-dark"
|
||||
popupContent="Partner plugin"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ResolvedTheme } from '@/features/theme/theme'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
type IconWithTooltipProps = {
|
||||
className?: string
|
||||
popupContent?: string
|
||||
theme: Theme
|
||||
theme: ResolvedTheme
|
||||
lightIconClassName: string
|
||||
darkIconClassName: string
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const IconWithTooltip: FC<IconWithTooltipProps> = ({
|
||||
lightIconClassName,
|
||||
darkIconClassName,
|
||||
}) => {
|
||||
const isDark = theme === Theme.dark
|
||||
const isDark = theme === 'dark'
|
||||
const iconClassName = cn(isDark ? darkIconClassName : lightIconClassName, 'size-5', className)
|
||||
const icon = (
|
||||
<span className="flex shrink-0 items-center justify-center">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import IconWithTooltip from './icon-with-tooltip'
|
||||
|
||||
type PartnerProps = {
|
||||
@@ -10,7 +10,7 @@ type PartnerProps = {
|
||||
}
|
||||
|
||||
const Partner: FC<PartnerProps> = ({ className, text }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
return (
|
||||
<IconWithTooltip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FC } from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import IconWithTooltip from './icon-with-tooltip'
|
||||
|
||||
type VerifiedProps = {
|
||||
@@ -8,7 +8,7 @@ type VerifiedProps = {
|
||||
}
|
||||
|
||||
const Verified: FC<VerifiedProps> = ({ className, text }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
return (
|
||||
<IconWithTooltip
|
||||
|
||||
@@ -6,9 +6,8 @@ import * as React from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { currentWorkspaceIdAtom } from '@/context/workspace-state'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { renderI18nObject } from '@/i18n/metadata'
|
||||
import { Theme } from '@/types/app'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Partner from '../base/badges/partner'
|
||||
import Verified from '../base/badges/verified'
|
||||
@@ -93,8 +92,8 @@ const Card = ({
|
||||
const { categoriesMap } = useCategories(true)
|
||||
const { category, type, name, org, label, brief, icon, icon_dark, verified, from } = payload
|
||||
const badges = payload.badges ?? []
|
||||
const { theme } = useTheme()
|
||||
const activeIcon = theme === Theme.dark && icon_dark ? icon_dark : icon
|
||||
const theme = useResolvedTheme()
|
||||
const activeIcon = theme === 'dark' && icon_dark ? icon_dark : icon
|
||||
const pluginIdentity = { from, name, org, type }
|
||||
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
||||
obj ? renderI18nObject(obj, locale) : ''
|
||||
@@ -133,13 +132,13 @@ const Card = ({
|
||||
</div>
|
||||
{isPartner && (
|
||||
<Partner
|
||||
className="ml-0.5 size-4"
|
||||
className="ms-0.5 size-4"
|
||||
text={t(($) => $['marketplace.partnerTip'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
{verified && (
|
||||
<Verified
|
||||
className="ml-0.5 size-4"
|
||||
className="ms-0.5 size-4"
|
||||
text={t(($) => $['marketplace.verifiedTip'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
@@ -173,7 +172,7 @@ const Card = ({
|
||||
text={getLocalizedText(brief)}
|
||||
descriptionLineRows={effectiveDescriptionLineRows}
|
||||
/>
|
||||
{!!footer && <div className="px-4 pt-2 pr-5">{footer}</div>}
|
||||
{!!footer && <div className="px-4 pe-5 pt-2">{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -191,18 +190,18 @@ const Card = ({
|
||||
installFailed={installFailed}
|
||||
plugin={pluginIdentity}
|
||||
/>
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="ms-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<Title title={getLocalizedText(label)} />
|
||||
{isPartner && (
|
||||
<Partner
|
||||
className="ml-0.5 size-4"
|
||||
className="ms-0.5 size-4"
|
||||
text={t(($) => $['marketplace.partnerTip'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
{verified && (
|
||||
<Verified
|
||||
className="ml-0.5 size-4"
|
||||
className="ms-0.5 size-4"
|
||||
text={t(($) => $['marketplace.verifiedTip'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { ReactElement } from 'react'
|
||||
import type { consoleClient } from '@/service/console'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { PluginInstallPermissionProvider } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createDeferred } from '@/test/deferred'
|
||||
@@ -67,7 +67,7 @@ describe('MarketplaceDetailDialog', () => {
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
render(
|
||||
<ThemeProvider defaultTheme="dark" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="dark">
|
||||
<MarketplaceDetailDialog open isInstalled plugin={plugin} onOpenChange={onOpenChange} />
|
||||
</ThemeProvider>,
|
||||
)
|
||||
@@ -90,7 +90,7 @@ describe('MarketplaceDetailDialog', () => {
|
||||
|
||||
it('installs from the embedded detail frame without opening a confirmation dialog', async () => {
|
||||
render(
|
||||
<ThemeProvider defaultTheme="dark" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="dark">
|
||||
<MarketplaceDetailDialog open isInstalled={false} plugin={plugin} onOpenChange={vi.fn()} />
|
||||
</ThemeProvider>,
|
||||
)
|
||||
@@ -154,7 +154,7 @@ describe('MarketplaceDetailDialog', () => {
|
||||
it('does not install when the workspace lacks plugin.install', async () => {
|
||||
render(
|
||||
<PluginInstallPermissionProvider canInstallPlugin={false}>
|
||||
<ThemeProvider defaultTheme="dark" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="dark">
|
||||
<MarketplaceDetailDialog
|
||||
open
|
||||
isInstalled={false}
|
||||
@@ -202,7 +202,7 @@ describe('MarketplaceDetailDialog', () => {
|
||||
|
||||
try {
|
||||
render(
|
||||
<ThemeProvider defaultTheme="dark" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="dark">
|
||||
<MarketplaceDetailDialog
|
||||
open
|
||||
isInstalled={false}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useRefWithInit } from '@langgenius/dify-ui/use-ref-with-init'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import { useOptionalPluginInstallPermission } from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission'
|
||||
import { marketplaceLinks } from '@/features/marketplace/links'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import MarketplaceDetailDialogFrame from './frame'
|
||||
import { useSilentMarketplaceInstall } from './use-silent-install'
|
||||
|
||||
@@ -120,9 +120,7 @@ function MarketplaceDetailDialog({
|
||||
const { t } = useTranslation(['plugin'])
|
||||
const locale = useLocale()
|
||||
const { canInstallPlugin } = useOptionalPluginInstallPermission()
|
||||
// resolvedTheme maps the "system" preference to the concrete light/dark
|
||||
// value the marketplace page expects.
|
||||
const { resolvedTheme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const pluginLabel = plugin.label[locale] ?? plugin.label['en-US'] ?? plugin.name
|
||||
const detailLabel = t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })
|
||||
const installedForSrcRef = useRef(isInstalled)
|
||||
@@ -132,7 +130,7 @@ function MarketplaceDetailDialog({
|
||||
installed: String(installedForSrcRef.current),
|
||||
language: locale,
|
||||
source: globalThis.location?.origin,
|
||||
theme: resolvedTheme,
|
||||
theme,
|
||||
view: 'modal',
|
||||
})
|
||||
const title = `${pluginLabel} · ${detailLabel}`
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
type LineProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Line = ({ className }: LineProps) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const isDarkMode = theme === 'dark'
|
||||
|
||||
if (isDarkMode) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import {
|
||||
createPluginInstallPermissionStore,
|
||||
PluginInstallPermissionContext,
|
||||
} from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createMarketplacePluginFixture } from '@/test/fixtures/plugin'
|
||||
@@ -65,7 +65,7 @@ describe('CardWrapper', () => {
|
||||
seedModelProviderQueries(queryClient)
|
||||
const store = createPluginInstallPermissionStore({ canInstallPlugin })
|
||||
return renderWithConsoleQuery(
|
||||
<ThemeProvider defaultTheme="dark" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="dark">
|
||||
<PluginInstallPermissionContext value={store}>
|
||||
<CardWrapper plugin={plugin} {...props} />
|
||||
</PluginInstallPermissionContext>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback } from 'react'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import { marketplaceLinks } from '@/features/marketplace/links'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import MarketplaceDetailDialogFrame from '../detail-dialog/frame'
|
||||
|
||||
const MARKETPLACE_INSTALL_MESSAGE_TYPE = 'dify-marketplace:install-template'
|
||||
@@ -24,14 +24,12 @@ export default function TemplateDetailDialog({
|
||||
}: TemplateDetailDialogProps) {
|
||||
const { t } = useTranslation(['plugin'])
|
||||
const locale = useLocale()
|
||||
// resolvedTheme maps the "system" preference to the concrete light/dark
|
||||
// value the marketplace page expects.
|
||||
const { resolvedTheme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const detailLabel = t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })
|
||||
const detailURL = marketplaceLinks.template(template, {
|
||||
language: locale,
|
||||
source: globalThis.location?.origin,
|
||||
theme: resolvedTheme,
|
||||
theme,
|
||||
view: 'modal',
|
||||
})
|
||||
const handleMessage = useCallback(
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import type { MarketplacePlugin, MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { useLocale } from '#i18n'
|
||||
import { marketplaceLinks } from '@/features/marketplace/links'
|
||||
import { deploymentEditionAtom } from '@/features/system-features/state'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
const subscribe = () => () => {}
|
||||
const getOrigin = () => window.location.origin
|
||||
@@ -23,11 +23,11 @@ type TemplateDetail = Pick<
|
||||
export function useMarketplaceDetailNavigation() {
|
||||
const edition = useAtomValue(deploymentEditionAtom)
|
||||
const language = useLocale()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const source = useSyncExternalStore(subscribe, getOrigin, getServerOrigin)
|
||||
const opensExternally = edition === 'COMMUNITY' || edition === 'ENTERPRISE'
|
||||
|
||||
const detailParams = { language, theme: resolvedTheme }
|
||||
const detailParams = { language, theme }
|
||||
const pluginHref = (plugin: PluginDetail) =>
|
||||
opensExternally && plugin.type !== 'bundle'
|
||||
? marketplaceLinks.officialPluginDetail(plugin, detailParams, source)
|
||||
|
||||
@@ -28,7 +28,7 @@ import { API_PREFIX } from '@/config'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useMarketplace, useMarketplacePluginVersions } from '@/features/marketplace/client'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { isEarlierThanVersion } from '@/utils/semver'
|
||||
@@ -86,7 +86,7 @@ const DetailHeader = ({
|
||||
...userProfileQueryOptions(),
|
||||
select: (data) => data.profile.timezone ?? undefined,
|
||||
})
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const locale = useGetLanguage()
|
||||
const currentLocale = useLocale()
|
||||
const marketplace = useMarketplace()
|
||||
@@ -200,13 +200,13 @@ const DetailHeader = ({
|
||||
</div>
|
||||
|
||||
{/* Plugin Info */}
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="ms-3 w-0 grow">
|
||||
{/* Title Row */}
|
||||
<div className="flex h-5 items-center">
|
||||
<Title title={label[locale]} />
|
||||
{verified && !isReadmeView && (
|
||||
<Verified
|
||||
className="ml-0.5 size-4"
|
||||
className="ms-0.5 size-4"
|
||||
text={t(($) => $['marketplace.verifiedTip'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
@@ -227,7 +227,7 @@ const DetailHeader = ({
|
||||
className="relative mx-1 h-5 overflow-visible rounded-[5px] border border-divider-deep px-1.25 system-xs-medium text-text-tertiary"
|
||||
>
|
||||
{hasNewVersion && (
|
||||
<span className="absolute -top-0.5 -right-0.5 size-1.5 rounded-xs border border-components-badge-status-light-error-border-inner bg-components-badge-status-light-error-bg shadow-sm" />
|
||||
<span className="absolute -inset-e-0.5 -top-0.5 size-1.5 rounded-xs border border-components-badge-status-light-error-border-inner bg-components-badge-status-light-error-bg shadow-sm" />
|
||||
)}
|
||||
<span>{isFromGitHub ? (meta?.version ?? version) : version}</span>
|
||||
{canChangeVersion && !loadingVersions && (
|
||||
@@ -283,7 +283,7 @@ const DetailHeader = ({
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div>
|
||||
<Badge className="mr-1 cursor-pointer px-1">
|
||||
<Badge className="me-1 cursor-pointer px-1">
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-custom-vender-system-auto-update-line size-3"
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocale } from '#i18n'
|
||||
import { useMarketplace } from '@/features/marketplace/client'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { PluginSource } from '../types'
|
||||
|
||||
type OperationMenuProps = Readonly<
|
||||
@@ -55,7 +55,7 @@ export function OperationMenu({
|
||||
const { t } = useTranslation(['plugin'])
|
||||
const marketplace = useMarketplace()
|
||||
const locale = useLocale()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const showInfo = source === PluginSource.github
|
||||
const showCheckVersionAction = showCheckVersion && source === PluginSource.github
|
||||
const detailUrl = !marketplace
|
||||
|
||||
@@ -19,8 +19,8 @@ import PluginItem from '../index'
|
||||
const mockEnableMarketplace = vi.fn(() => true)
|
||||
|
||||
const mockTheme = vi.fn(() => 'light')
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: mockTheme() }),
|
||||
vi.mock('@/features/theme/hooks', () => ({
|
||||
useResolvedTheme: () => mockTheme(),
|
||||
}))
|
||||
const PluginPageContextProvider = PluginPageContext.Provider
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useMarketplace } from '@/features/marketplace/client'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { isEqualOrLaterThanVersion } from '@/utils/semver'
|
||||
import Badge from '../../base/badge'
|
||||
import Verified from '../base/badges/verified'
|
||||
@@ -44,7 +44,7 @@ const PluginItem: FC<Props> = ({
|
||||
plugin,
|
||||
}) => {
|
||||
const { t } = useTranslation(['plugin'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const selectedPluginID = usePluginPageContext((v) =>
|
||||
v.selectedItem?.type === 'plugin' ? v.selectedItem.id : undefined,
|
||||
)
|
||||
@@ -150,14 +150,14 @@ const PluginItem: FC<Props> = ({
|
||||
width={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="ms-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<div id={titleId} className="min-w-0">
|
||||
<Title title={title} />
|
||||
</div>
|
||||
{verified && (
|
||||
<Verified
|
||||
className="ml-0.5 size-4"
|
||||
className="ms-0.5 size-4"
|
||||
text={t(($) => $['marketplace.verifiedTip'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
@@ -169,7 +169,7 @@ const PluginItem: FC<Props> = ({
|
||||
ns: 'plugin',
|
||||
minimalDifyVersion: declarationMeta.minimum_dify_version ?? '0.0.0',
|
||||
})}
|
||||
className="relative z-20 ml-0.5 inline-flex size-4 shrink-0 border-0 bg-transparent p-0"
|
||||
className="relative z-20 ms-0.5 inline-flex size-4 shrink-0 border-0 bg-transparent p-0"
|
||||
>
|
||||
<RiErrorWarningLine color="red" className="size-4 text-text-accent" />
|
||||
</PopoverTrigger>
|
||||
@@ -182,7 +182,7 @@ const PluginItem: FC<Props> = ({
|
||||
</Popover>
|
||||
)}
|
||||
<Badge
|
||||
className="ml-1 shrink-0"
|
||||
className="ms-1 shrink-0"
|
||||
text={source === PluginSource.github ? plugin.meta!.version : plugin.version}
|
||||
hasRedCornerMark={
|
||||
source === PluginSource.marketplace &&
|
||||
@@ -193,11 +193,11 @@ const PluginItem: FC<Props> = ({
|
||||
</div>
|
||||
<div className="relative flex h-4 min-w-0 items-center">
|
||||
<Description
|
||||
className="min-w-0 flex-1 group-focus-within/plugin-item:pr-20 group-hover/plugin-item:pr-20 [@media(hover:none)]:pr-20"
|
||||
className="min-w-0 flex-1 group-focus-within/plugin-item:pe-20 group-hover/plugin-item:pe-20 [@media(hover:none)]:pe-20"
|
||||
text={descriptionText}
|
||||
descriptionLineRows={1}
|
||||
/>
|
||||
<div className="pointer-events-none absolute top-1/2 right-0 z-20 -translate-y-1/2 opacity-0 transition-opacity group-focus-within/plugin-item:pointer-events-auto group-focus-within/plugin-item:opacity-100 group-hover/plugin-item:pointer-events-auto group-hover/plugin-item:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100">
|
||||
<div className="pointer-events-none absolute inset-e-0 top-1/2 z-20 -translate-y-1/2 opacity-0 transition-opacity group-focus-within/plugin-item:pointer-events-auto group-focus-within/plugin-item:opacity-100 group-hover/plugin-item:pointer-events-auto group-hover/plugin-item:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100">
|
||||
<Action
|
||||
pluginUniqueIdentifier={plugin_unique_identifier}
|
||||
installationId={installation_id}
|
||||
@@ -303,7 +303,7 @@ const PluginItem: FC<Props> = ({
|
||||
</div>
|
||||
{/* BG Effect for Deprecated Plugin */}
|
||||
{source === PluginSource.marketplace && marketplace && isDeprecated && (
|
||||
<div className="absolute -right-11.25 -bottom-17.75 z-0 size-40 bg-components-badge-status-light-warning-halo opacity-60 blur-[120px]" />
|
||||
<div className="absolute -inset-e-11.25 -bottom-17.75 z-0 size-40 bg-components-badge-status-light-warning-halo opacity-60 blur-[120px]" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ import type { ConsoleQueryTestOptions } from '@/test/console/query-data'
|
||||
import { zGetWorkspacesCurrentPluginMarketplacePkgResponse } from '@dify/contracts/api/console/workspaces/zod.gen'
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { marketplaceLinks } from '@/features/marketplace/links'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { seedModelProviderQueries } from '@/test/console/model-provider'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createMarketplacePluginFixture, createPluginEntity } from '@/test/fixtures/plugin'
|
||||
@@ -67,7 +67,7 @@ const setup = (
|
||||
const queryClient = createConsoleQueryClient()
|
||||
seedModelProviderQueries(queryClient)
|
||||
const rendered = renderWithConsoleQuery(
|
||||
<ThemeProvider defaultTheme="light" forcedTheme="light" enableSystem={false}>
|
||||
<ThemeProvider defaultPreference="light">
|
||||
<CategoryMarketplacePanel
|
||||
marketplace={marketplaceLinks}
|
||||
category={Category.trigger}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
@@ -16,6 +15,7 @@ import List from '@/app/components/plugins/marketplace/list'
|
||||
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/query'
|
||||
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { getCategoryMarketplaceId } from './category-marketplace'
|
||||
@@ -34,7 +34,7 @@ const CategoryMarketplacePanel = ({
|
||||
tags?: string[]
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'plugin', 'workflow', 'modelProvider'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { canInstallPlugin } = usePluginSettingsAccess()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const {
|
||||
@@ -103,7 +103,7 @@ const CategoryMarketplacePanel = ({
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={!collapsed}
|
||||
className="flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-left system-md-semibold text-text-primary"
|
||||
className="flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-start system-md-semibold text-text-primary"
|
||||
onClick={() => setCollapsed((value) => !value)}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { MarketplaceLinks } from '@/features/marketplace/links'
|
||||
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTheme } from 'next-themes'
|
||||
import * as React from 'react'
|
||||
import { useId, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -13,6 +12,7 @@ import { useLocale } from '#i18n'
|
||||
import usePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { useMarketplaceDetailNavigation } from '@/app/components/plugins/marketplace/use-detail-navigation'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import Badge from '../base/badge'
|
||||
import Icon from './card/base/card-icon'
|
||||
@@ -28,7 +28,7 @@ type Props = Readonly<{
|
||||
const ProviderCardComponent: FC<Props> = ({ className, marketplace, payload }) => {
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation(['plugin'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const [
|
||||
isShowInstallFromMarketplace,
|
||||
{ setTrue: showInstallFromMarketplace, setFalse: hideInstallFromMarketplace },
|
||||
@@ -54,7 +54,7 @@ const ProviderCardComponent: FC<Props> = ({ className, marketplace, payload }) =
|
||||
{/* Header */}
|
||||
<div className="flex">
|
||||
<Icon src={payload.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="ms-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<h2
|
||||
id={titleId}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
const PipelineScreenShot = () => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
return (
|
||||
<picture>
|
||||
|
||||
@@ -49,12 +49,12 @@ describe('TextGenerationMenu', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TextGenerationMenu data={siteInfo} />)
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
expect(screen.getByRole('menuitemradio', { name: 'common.theme.auto' })).toHaveAttribute(
|
||||
expect(screen.getByRole('menuitemradio', { name: 'common.theme.system' })).toHaveAttribute(
|
||||
'aria-checked',
|
||||
'true',
|
||||
)
|
||||
await user.click(screen.getByRole('menuitemradio', { name: 'common.theme.light' }))
|
||||
expect(mockSetTheme).toHaveBeenCalledWith('light', expect.anything())
|
||||
expect(mockSetTheme).toHaveBeenCalledWith('light')
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import type { MenuContentProps } from '@langgenius/dify-ui/menu'
|
||||
import type { ThemePreference } from '@/features/theme/theme'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import {
|
||||
@@ -13,11 +14,12 @@ import {
|
||||
MenuSeparator,
|
||||
MenuTrigger,
|
||||
} from '@langgenius/dify-ui/menu'
|
||||
import { useTheme } from 'next-themes'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useThemePreference } from '@/features/theme/hooks'
|
||||
import { themeOptions } from '@/features/theme/theme'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { resolveWebAppAddress } from '@/service/webapp-address'
|
||||
@@ -42,7 +44,7 @@ function TextGenerationMenu({ data, side, align, hideLogout }: Props) {
|
||||
router.replace(`/webapp-signin?redirect_url=${pathname}`)
|
||||
}
|
||||
|
||||
const { theme, setTheme } = useTheme()
|
||||
const { preference, setPreference } = useThemePreference()
|
||||
const [show, setShow] = useState(false)
|
||||
const handleOpenInfoModal = () => {
|
||||
queueMicrotask(() => {
|
||||
@@ -65,53 +67,34 @@ function TextGenerationMenu({ data, side, align, hideLogout }: Props) {
|
||||
}
|
||||
/>
|
||||
<MenuContent side={side} align={align} className="w-56">
|
||||
<MenuRadioGroup
|
||||
value={theme ?? 'system'}
|
||||
onValueChange={setTheme}
|
||||
<MenuRadioGroup<ThemePreference>
|
||||
value={preference}
|
||||
onValueChange={setPreference}
|
||||
className="flex items-center gap-2 px-3 system-md-regular text-text-secondary"
|
||||
>
|
||||
<MenuGroupLabel className="grow px-0 py-0 system-md-regular text-text-secondary normal-case">
|
||||
{t(($) => $['theme.theme'], { ns: 'common' })}
|
||||
{t(($) => $['theme.label'], { ns: 'common' })}
|
||||
</MenuGroupLabel>
|
||||
<div className="flex items-center rounded-[10px] bg-components-segmented-control-bg-normal p-0.5">
|
||||
{(['system', 'light', 'dark'] as const).map((value, index) => (
|
||||
<React.Fragment key={value}>
|
||||
{themeOptions.map((option, index) => (
|
||||
<React.Fragment key={option.value}>
|
||||
{index > 0 && (
|
||||
<div
|
||||
aria-hidden
|
||||
className={
|
||||
(index === 1 && theme === 'dark') ||
|
||||
(index === 2 && (theme ?? 'system') === 'system')
|
||||
preference !== option.value && preference !== themeOptions[index - 1]?.value
|
||||
? 'h-3.5 w-px bg-divider-regular'
|
||||
: 'h-3.5 w-px bg-transparent'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<MenuRadioItem
|
||||
value={value}
|
||||
aria-label={t(
|
||||
($) =>
|
||||
$[
|
||||
value === 'system'
|
||||
? 'theme.auto'
|
||||
: value === 'light'
|
||||
? 'theme.light'
|
||||
: 'theme.dark'
|
||||
],
|
||||
{ ns: 'common' },
|
||||
)}
|
||||
<MenuRadioItem<ThemePreference>
|
||||
value={option.value}
|
||||
aria-label={t(($) => $[option.labelKey], { ns: 'common' })}
|
||||
className="mx-0 h-auto py-1 text-text-tertiary data-checked:bg-components-segmented-control-item-active-bg data-checked:text-text-accent-light-mode-only data-checked:shadow-sm data-highlighted:text-text-secondary data-checked:data-highlighted:bg-components-segmented-control-item-active-bg data-checked:data-highlighted:text-text-accent-light-mode-only"
|
||||
>
|
||||
<span aria-hidden className="p-0.5">
|
||||
<span
|
||||
className={
|
||||
value === 'system'
|
||||
? 'i-ri-computer-line size-4'
|
||||
: value === 'light'
|
||||
? 'i-ri-sun-line size-4'
|
||||
: 'i-ri-moon-line size-4'
|
||||
}
|
||||
/>
|
||||
<span className={`${option.iconClassName} size-4`} />
|
||||
</span>
|
||||
</MenuRadioItem>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { useToolMarketplace } from './hooks'
|
||||
import type { MarketplaceLinks } from '@/features/marketplace/links'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocale } from '#i18n'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import List from '@/app/components/plugins/marketplace/list'
|
||||
import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '../content-inset'
|
||||
|
||||
@@ -33,7 +33,7 @@ const Marketplace = ({
|
||||
}: MarketplaceProps) => {
|
||||
const locale = useLocale()
|
||||
const { t } = useTranslation(['common', 'plugin'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const router = useRouter()
|
||||
const { canInstallPlugin } = usePluginSettingsAccess()
|
||||
const { isLoading, marketplaceCollections, marketplaceCollectionPluginsMap, plugins, page } =
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RiArrowRightLine, RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { ToolType } from '../../workflow/block-selector/types'
|
||||
|
||||
@@ -32,7 +32,7 @@ const getLink = (type?: ToolType) => {
|
||||
const Empty = ({ type, isAgent }: Props) => {
|
||||
const { t } = useTranslation(['tools'])
|
||||
const docLink = useDocLink()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
const hasLink = type === ToolType.Custom || type === ToolType.MCP
|
||||
const renderType = isAgent ? ('agent' as const) : type
|
||||
|
||||
@@ -33,13 +33,13 @@ import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { BlockEnum, InputVarType, isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
|
||||
const FeaturesTrigger = () => {
|
||||
const { t } = useTranslation(['common', 'workflow'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const isChatflow = useAtomValue(isChatflowAtom)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { MarketplaceLinks } from '@/features/marketplace/links'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { Menu, MenuContent, MenuItem, MenuLinkItem, MenuTrigger } from '@langgenius/dify-ui/menu'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { marketplaceQuery } from '@/service/marketplace'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
|
||||
@@ -20,7 +20,7 @@ type Props = Readonly<{
|
||||
|
||||
function OperationMenu({ marketplace, open, onOpenChange, author, name, version }: Props) {
|
||||
const { t } = useTranslation(['common'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
const downloadMutation = useMutation(
|
||||
marketplaceQuery.downloadPlugin.mutationOptions({
|
||||
|
||||
@@ -12,8 +12,7 @@ import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { getProviderReference } from '@/utils/provider-reference'
|
||||
import { basePath } from '@/utils/var'
|
||||
import BlockIcon from '../../block-icon'
|
||||
@@ -62,7 +61,7 @@ const ToolItem: FC<Props> = ({
|
||||
const language = useGetLanguage()
|
||||
const previewDescriptionId = React.useId()
|
||||
const previewDescription = payload.description[language]
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
return normalizeProviderIcon(provider.icon) ?? provider.icon
|
||||
}, [provider.icon])
|
||||
@@ -71,7 +70,7 @@ const ToolItem: FC<Props> = ({
|
||||
return normalizeProviderIcon(provider.icon_dark) ?? provider.icon_dark
|
||||
}, [provider.icon_dark])
|
||||
const providerIcon = useMemo(() => {
|
||||
if (theme === Theme.dark && normalizedIconDark) return normalizedIconDark
|
||||
if (theme === 'dark' && normalizedIconDark) return normalizedIconDark
|
||||
return normalizedIcon
|
||||
}, [theme, normalizedIcon, normalizedIconDark])
|
||||
|
||||
@@ -81,7 +80,7 @@ const ToolItem: FC<Props> = ({
|
||||
size="medium"
|
||||
aria-describedby={previewDescription ? previewDescriptionId : undefined}
|
||||
disabled={disabled}
|
||||
className="w-full justify-between pr-1 pl-5.25 text-left focus-visible:ring-inset disabled:cursor-default"
|
||||
className="w-full justify-between ps-5.25 pe-1 text-start focus-visible:ring-inset disabled:cursor-default"
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
const params: Record<string, string> = {}
|
||||
@@ -114,11 +113,11 @@ const ToolItem: FC<Props> = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className="truncate border-l-2 border-divider-subtle py-2 pl-4 system-sm-medium text-text-secondary">
|
||||
<div className="truncate border-s-2 border-divider-subtle py-2 ps-4 system-sm-medium text-text-secondary">
|
||||
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
|
||||
</div>
|
||||
{isAdded && (
|
||||
<div className="mr-4 system-xs-regular text-text-tertiary">
|
||||
<div className="me-4 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['addToolModal.added'], { ns: 'tools' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,8 +11,7 @@ import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { getProviderReference, matchesProviderReference } from '@/utils/provider-reference'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { CollectionType } from '../../../tools/types'
|
||||
@@ -69,7 +68,7 @@ function Tool({
|
||||
const [previousHasSearchText, setPreviousHasSearchText] = React.useState(hasSearchText)
|
||||
const isMCPTool = payload.type === CollectionType.mcp
|
||||
const isShowCanNotChooseMCPTip = !isMCPToolAllowed && isMCPTool
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
return normalizeProviderIcon(payload.icon) ?? payload.icon
|
||||
}, [payload.icon])
|
||||
@@ -78,7 +77,7 @@ function Tool({
|
||||
return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark
|
||||
}, [payload.icon_dark])
|
||||
const providerIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
if (theme === Theme.dark && normalizedIconDark) return normalizedIconDark
|
||||
if (theme === 'dark' && normalizedIconDark) return normalizedIconDark
|
||||
return normalizedIcon
|
||||
}, [theme, normalizedIcon, normalizedIconDark])
|
||||
const getIsDisabled = useCallback(
|
||||
@@ -120,17 +119,17 @@ function Tool({
|
||||
className={cn('flex h-8 min-w-0 grow items-center', isShowCanNotChooseMCPTip && 'opacity-30')}
|
||||
>
|
||||
<BlockIcon className="shrink-0" type={BlockEnum.Tool} toolIcon={providerIcon} />
|
||||
<div className="ml-2 flex w-0 grow items-center text-sm text-text-primary">
|
||||
<div className="ms-2 flex w-0 grow items-center text-sm text-text-primary">
|
||||
<span className="max-w-62.5 truncate">
|
||||
{notShowProvider ? actions[0]?.label[language] : payload.label[language]}
|
||||
</span>
|
||||
{isFlatView && groupName && (
|
||||
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{groupName}</span>
|
||||
<span className="ms-2 shrink-0 system-xs-regular text-text-quaternary">{groupName}</span>
|
||||
)}
|
||||
{isMCPTool && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="ml-2 i-custom-vender-other-mcp size-3.5 shrink-0 text-text-quaternary"
|
||||
className="ms-2 i-custom-vender-other-mcp size-3.5 shrink-0 text-text-quaternary"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -204,12 +203,12 @@ function Tool({
|
||||
<div className={cn('mb-1 last-of-type:mb-0', className)}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg border-0 bg-transparent pr-2 pl-3 text-left hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden"
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg border-0 bg-transparent ps-3 pe-2 text-start hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={handleWorkflowToolSelect}
|
||||
>
|
||||
{providerDetails}
|
||||
{!canNotSelectMultiple && selectedStatus && (
|
||||
<span className="ml-2 shrink-0 system-xs-regular text-text-tertiary">
|
||||
<span className="ms-2 shrink-0 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['addToolModal.added'], { ns: 'tools' })}
|
||||
</span>
|
||||
)}
|
||||
@@ -227,13 +226,13 @@ function Tool({
|
||||
<div className="group/item relative flex w-full items-center rounded-lg">
|
||||
<CollapsibleTrigger
|
||||
aria-controls={panelId}
|
||||
className="group/collapsible flex h-8 min-h-8 w-full min-w-0 touch-manipulation items-center justify-start gap-0 rounded-lg bg-transparent pr-2 pl-3 text-start system-sm-medium text-text-secondary select-none group-hover/item:bg-state-base-hover hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-inset data-panel-open:text-text-primary"
|
||||
className="group/collapsible flex h-8 min-h-8 w-full min-w-0 touch-manipulation items-center justify-start gap-0 rounded-lg bg-transparent ps-3 pe-2 text-start system-sm-medium text-text-secondary select-none group-hover/item:bg-state-base-hover hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-inset data-panel-open:text-text-primary"
|
||||
>
|
||||
{providerDetails}
|
||||
{!isShowCanNotChooseMCPTip && !canNotSelectMultiple && selectedStatus && (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-2 shrink-0 system-xs-regular text-text-tertiary',
|
||||
'ms-2 shrink-0 system-xs-regular text-text-tertiary',
|
||||
!isAllSelected &&
|
||||
'group-focus-within/item:hidden group-hover/item:hidden [@media(hover:none)]:hidden',
|
||||
)}
|
||||
@@ -244,13 +243,13 @@ function Tool({
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'ml-2 i-ri-arrow-right-s-line size-4 shrink-0 text-text-quaternary transition-transform group-data-panel-open/collapsible:rotate-90 motion-reduce:transition-none',
|
||||
'ms-2 i-ri-arrow-right-s-line size-4 shrink-0 text-text-quaternary transition-transform group-data-panel-open/collapsible:rotate-90 motion-reduce:transition-none',
|
||||
!isShowCanNotChooseMCPTip &&
|
||||
!canNotSelectMultiple &&
|
||||
!isAllSelected &&
|
||||
onSelectMultiple &&
|
||||
'group-focus-within/item:hidden group-hover/item:hidden [@media(hover:none)]:hidden',
|
||||
isShowCanNotChooseMCPTip && 'mr-7',
|
||||
isShowCanNotChooseMCPTip && 'me-7',
|
||||
)}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
@@ -262,7 +261,7 @@ function Tool({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 text-components-button-secondary-accent-text opacity-0 group-focus-within/item:pointer-events-auto group-focus-within/item:opacity-100 group-hover/item:pointer-events-auto group-hover/item:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100"
|
||||
className="pointer-events-none absolute inset-e-1 top-1/2 -translate-y-1/2 text-components-button-secondary-accent-text opacity-0 group-focus-within/item:pointer-events-auto group-focus-within/item:opacity-100 group-hover/item:pointer-events-auto group-hover/item:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleSelectAll()
|
||||
@@ -272,7 +271,7 @@ function Tool({
|
||||
</Button>
|
||||
)}
|
||||
{isShowCanNotChooseMCPTip && (
|
||||
<div className="absolute top-1/2 right-3 flex -translate-y-1/2">
|
||||
<div className="absolute inset-e-3 top-1/2 flex -translate-y-1/2">
|
||||
<McpToolNotSupportTooltip />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,8 +21,7 @@ import { CollectionType } from '@/app/components/tools/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { BlockSelectorRow } from '../block-selector-row'
|
||||
import TriggerPluginActionItem from './action-item'
|
||||
@@ -58,7 +57,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const language = useGetLanguage()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
const actions = payload.events
|
||||
const hasAction = !notShowProvider
|
||||
@@ -92,7 +91,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark
|
||||
}, [payload.icon_dark])
|
||||
const providerIcon = useMemo<TriggerWithProvider['icon']>(() => {
|
||||
if (theme === Theme.dark && normalizedIconDark) return normalizedIconDark
|
||||
if (theme === 'dark' && normalizedIconDark) return normalizedIconDark
|
||||
return normalizedIcon
|
||||
}, [normalizedIcon, normalizedIconDark, theme])
|
||||
const providerWithResolvedIcon = useMemo(
|
||||
@@ -143,7 +142,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
>
|
||||
<div className="flex min-w-0 grow items-center">
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
className="me-2 shrink-0"
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size="sm"
|
||||
toolIcon={providerIcon}
|
||||
@@ -152,13 +151,13 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
<span className="max-w-50 truncate">
|
||||
{notShowProvider ? actions[0]?.label[language] : payload.label[language]}
|
||||
</span>
|
||||
<span className="ml-2 truncate system-xs-regular text-text-quaternary">
|
||||
<span className="ms-2 truncate system-xs-regular text-text-quaternary">
|
||||
{groupName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 flex items-center">
|
||||
<div className="ms-2 flex items-center">
|
||||
{hasAction && (
|
||||
<FoldIcon
|
||||
className={cn(
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const showChatVariablePanel = useStore((s) => s.showChatVariablePanel)
|
||||
const setShowChatVariablePanel = useStore((s) => s.setShowChatVariablePanel)
|
||||
|
||||
@@ -5,10 +5,10 @@ import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks/use-input-field-panel'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const setShowChatVariablePanel = useStore((s) => s.setShowChatVariablePanel)
|
||||
const showEnvPanel = useStore((s) => s.showEnvPanel)
|
||||
|
||||
@@ -5,10 +5,10 @@ import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks/use-input-field-panel'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
const GlobalVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const showGlobalVariablePanel = useStore((s) => s.showGlobalVariablePanel)
|
||||
const setShowGlobalVariablePanel = useStore((s) => s.setShowGlobalVariablePanel)
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getWorkflowVersionName } from '@/app/components/workflow/utils/version'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { deploymentEditionAtom } from '@/features/system-features/state'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import {
|
||||
useInvalidateAllLastRun,
|
||||
@@ -30,7 +30,7 @@ export type HeaderInRestoringProps = {
|
||||
}
|
||||
const HeaderInRestoring = ({ onRestoreSettled }: HeaderInRestoringProps) => {
|
||||
const { t } = useTranslation(['billing', 'workflow', 'workflowHistory'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false)
|
||||
const deploymentEdition = useAtomValue(deploymentEditionAtom)
|
||||
const { data: plan } = useQuery(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useHotkey } from '@tanstack/react-hotkeys'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { VERSION_HISTORY_HOTKEY } from '../hotkeys'
|
||||
import { ShortcutKbd } from '../shortcuts/shortcut-kbd'
|
||||
|
||||
@@ -12,7 +12,7 @@ type VersionHistoryButtonProps = {
|
||||
}
|
||||
|
||||
export function VersionHistoryButton({ onClick }: VersionHistoryButtonProps) {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { t } = useTranslation(['workflow'])
|
||||
const label = t(($) => $['common.versionHistory'], { ns: 'workflow' })
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ vi.mock('@/service/use-triggers', async () =>
|
||||
)
|
||||
|
||||
let mockTheme = 'light'
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: mockTheme }),
|
||||
vi.mock('@/features/theme/hooks', () => ({
|
||||
useResolvedTheme: () => mockTheme,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', async (importOriginal) => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useCallback, useMemo } from 'react'
|
||||
import { resolveDatasourceIcon } from '@/app/components/rag-pipeline/utils/datasource-icon'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { canFindTool } from '@/utils'
|
||||
@@ -189,7 +189,7 @@ export const useToolIcon = (data?: Node['data']) => {
|
||||
)
|
||||
const dataSourceList = useStore((s) => s.dataSourceList)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
const toolIcon = useMemo(() => {
|
||||
return (
|
||||
@@ -239,7 +239,7 @@ export const useGetToolIcon = () => {
|
||||
)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
|
||||
const getToolIcon = useCallback(
|
||||
(data: Node['data']) => {
|
||||
|
||||
@@ -8,8 +8,7 @@ import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { getFilesInLogs } from '@/app/components/base/file-uploader/utils'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Base from '../base'
|
||||
import { serializeCodeEditorValue } from './utils'
|
||||
@@ -76,7 +75,7 @@ const CodeEditor: FC<Props> = ({
|
||||
const [isMounted, setIsMounted] = React.useState(false)
|
||||
const minHeight = height || 200
|
||||
const [editorContentHeight, setEditorContentHeight] = useState(56)
|
||||
const { theme: appTheme } = useTheme()
|
||||
const appTheme = useResolvedTheme()
|
||||
const valueRef = useRef(value)
|
||||
useEffect(() => {
|
||||
valueRef.current = value
|
||||
@@ -113,7 +112,7 @@ const CodeEditor: FC<Props> = ({
|
||||
setIsFocus(false)
|
||||
})
|
||||
|
||||
monaco.editor.setTheme(appTheme === Theme.light ? 'light' : 'vs-dark') // Fix: sometimes not load the default theme
|
||||
monaco.editor.setTheme(appTheme === 'light' ? 'light' : 'vs-dark') // Fix: sometimes not load the default theme
|
||||
|
||||
onMount?.(editor, monaco)
|
||||
setIsMounted(true)
|
||||
@@ -122,7 +121,7 @@ const CodeEditor: FC<Props> = ({
|
||||
const outPutValue = serializeCodeEditorValue(value, isJSONStringifyBeauty)
|
||||
|
||||
const theme = useMemo(() => {
|
||||
if (appTheme === Theme.light) return 'light'
|
||||
if (appTheme === 'light') return 'light'
|
||||
return 'vs-dark'
|
||||
}, [appTheme])
|
||||
|
||||
@@ -157,7 +156,7 @@ const CodeEditor: FC<Props> = ({
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
{!outPutValue && !isFocus && (
|
||||
<div className="pointer-events-none absolute top-0 left-9 text-[13px] leading-4.5 font-normal text-components-input-text-placeholder">
|
||||
<div className="pointer-events-none absolute inset-s-9 top-0 text-[13px] leading-4.5 font-normal text-components-input-text-placeholder">
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+5
-6
@@ -6,8 +6,7 @@ import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
type CodeEditorProps = {
|
||||
value: string
|
||||
@@ -38,7 +37,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
|
||||
onBlur,
|
||||
}) => {
|
||||
const { t } = useTranslation(['common'])
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const monacoRef = useRef<Monaco | null>(null)
|
||||
const editorRef = useRef<MonacoEditor | null>(null)
|
||||
const [isMounted, setIsMounted] = React.useState(false)
|
||||
@@ -46,7 +45,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (monacoRef.current) {
|
||||
if (theme === Theme.light) monacoRef.current.editor.setTheme('light-theme')
|
||||
if (theme === 'light') monacoRef.current.editor.setTheme('light-theme')
|
||||
else monacoRef.current.editor.setTheme('dark-theme')
|
||||
}
|
||||
}, [theme])
|
||||
@@ -101,7 +100,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
|
||||
)
|
||||
|
||||
const editorTheme = useMemo(() => {
|
||||
if (theme === Theme.light) return 'light-theme'
|
||||
if (theme === 'light') return 'light-theme'
|
||||
return 'dark-theme'
|
||||
}, [theme])
|
||||
useEffect(() => {
|
||||
@@ -125,7 +124,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
|
||||
)}
|
||||
>
|
||||
{!hideTopMenu && (
|
||||
<div className="flex items-center justify-between pt-1 pr-1 pl-2">
|
||||
<div className="flex items-center justify-between ps-2 pe-1 pt-1">
|
||||
<div className="py-0.5 system-xs-semibold-uppercase text-text-secondary">
|
||||
<span className="px-1 py-0.5">JSON</span>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -11,9 +11,9 @@ import { useAutoGenModel } from '@/app/components/app/configuration/config/auto-
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { toast } from '@/app/notifications'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { ModelModeType, Theme } from '@/types/app'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
import { useMittContext } from '../visual-editor/context'
|
||||
import { useVisualEditorStore } from '../visual-editor/store'
|
||||
import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets'
|
||||
@@ -45,7 +45,7 @@ export function JsonSchemaGenerator({ onApply, alignOffset }: JsonSchemaGenerato
|
||||
const [model, setModel] = useAutoGenModel()
|
||||
const [instruction, setInstruction] = useState('')
|
||||
const [schema, setSchema] = useState<SchemaRoot | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const { defaultModel } = useModelListAndDefaultModelAndCurrentProviderAndModel(
|
||||
ModelTypeEnum.textGeneration,
|
||||
)
|
||||
@@ -63,7 +63,7 @@ export function JsonSchemaGenerator({ onApply, alignOffset }: JsonSchemaGenerato
|
||||
const advancedEditing = useVisualEditorStore((state) => state.advancedEditing)
|
||||
const isAddingNewField = useVisualEditorStore((state) => state.isAddingNewField)
|
||||
const { emit } = useMittContext()
|
||||
const SchemaGenerator = theme === Theme.light ? SchemaGeneratorLight : SchemaGeneratorDark
|
||||
const SchemaGenerator = theme === 'light' ? SchemaGeneratorLight : SchemaGeneratorDark
|
||||
|
||||
const handleTrigger = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { CopyFeedback } from '@/app/components/base/copy-feedback'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
|
||||
type Props = {
|
||||
readonly status: string
|
||||
@@ -12,7 +11,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const StatusContainer: FC<Props> = ({ status, children, copyContent }) => {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const isCopyable = copyContent !== undefined
|
||||
|
||||
return (
|
||||
@@ -20,68 +19,67 @@ const StatusContainer: FC<Props> = ({ status, children, copyContent }) => {
|
||||
role="status"
|
||||
className={cn(
|
||||
'group/status relative rounded-lg border border-workflow-display-disabled-border-1 px-3 py-2.5 system-xs-regular break-all',
|
||||
isCopyable && 'focus-within:pr-10 hover:pr-10 [@media(hover:none)]:pr-10',
|
||||
isCopyable && 'focus-within:pe-10 hover:pe-10 [@media(hover:none)]:pe-10',
|
||||
status === 'succeeded' &&
|
||||
'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'succeeded' &&
|
||||
theme === Theme.light &&
|
||||
theme === 'light' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'succeeded' &&
|
||||
theme === Theme.dark &&
|
||||
theme === 'dark' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
status === 'partial-succeeded' &&
|
||||
'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'partial-succeeded' &&
|
||||
theme === Theme.light &&
|
||||
theme === 'light' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'partial-succeeded' &&
|
||||
theme === Theme.dark &&
|
||||
theme === 'dark' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
status === 'failed' &&
|
||||
'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] text-text-warning',
|
||||
status === 'failed' &&
|
||||
theme === Theme.light &&
|
||||
theme === 'light' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'failed' &&
|
||||
theme === Theme.dark &&
|
||||
theme === 'dark' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(240,68,56,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
(status === 'stopped' || status === 'paused') &&
|
||||
'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
|
||||
(status === 'stopped' || status === 'paused') &&
|
||||
theme === Theme.light &&
|
||||
theme === 'light' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
(status === 'stopped' || status === 'paused') &&
|
||||
theme === Theme.dark &&
|
||||
theme === 'dark' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
status === 'exception' &&
|
||||
'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
|
||||
status === 'exception' &&
|
||||
theme === Theme.light &&
|
||||
theme === 'light' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'exception' &&
|
||||
theme === Theme.dark &&
|
||||
theme === 'dark' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
status === 'running' &&
|
||||
'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] text-util-colors-blue-light-blue-light-600',
|
||||
status === 'running' &&
|
||||
theme === Theme.light &&
|
||||
theme === 'light' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'running' &&
|
||||
theme === Theme.dark &&
|
||||
theme === 'dark' &&
|
||||
'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(11,165,236,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute top-0 left-0 h-12.5 w-[65%] bg-no-repeat',
|
||||
theme === Theme.light && 'bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]',
|
||||
theme === Theme.dark &&
|
||||
'bg-[url(~@/app/components/workflow/run/assets/highlight-dark.svg)]',
|
||||
'pointer-events-none absolute inset-s-0 top-0 h-12.5 w-[65%] bg-no-repeat',
|
||||
theme === 'light' && 'bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]',
|
||||
theme === 'dark' && 'bg-[url(~@/app/components/workflow/run/assets/highlight-dark.svg)]',
|
||||
)}
|
||||
></div>
|
||||
{children}
|
||||
{isCopyable && (
|
||||
<div className="pointer-events-none absolute top-1.5 right-1.5 z-10 opacity-0 transition-opacity group-focus-within/status:pointer-events-auto group-focus-within/status:opacity-100 group-hover/status:pointer-events-auto group-hover/status:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100">
|
||||
<div className="pointer-events-none absolute inset-e-1.5 top-1.5 z-10 opacity-0 transition-opacity group-focus-within/status:pointer-events-auto group-focus-within/status:opacity-100 group-hover/status:pointer-events-auto group-hover/status:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100">
|
||||
<CopyFeedback content={copyContent} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-10
@@ -1,10 +1,8 @@
|
||||
import type { ThemeProviderProps } from 'next-themes'
|
||||
import type { Metadata, Viewport } from '@/next'
|
||||
import { TooltipProvider } from '@langgenius/dify-ui/tooltip'
|
||||
import { HydrationBoundary } from '@tanstack/react-query'
|
||||
import { dir } from 'i18next'
|
||||
import { Provider as JotaiProvider } from 'jotai/react'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
||||
import { AppToaster } from '@/app/notifications/app-toaster'
|
||||
import { IS_PROD } from '@/config'
|
||||
@@ -16,6 +14,7 @@ import {
|
||||
dehydrateSystemFeatures,
|
||||
getOptionalSystemFeatures,
|
||||
} from '@/features/system-features/server'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { headers } from '@/next/headers'
|
||||
import { getApplicationTitle } from '@/utils/document-title'
|
||||
@@ -58,13 +57,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
const datasetMap = getDatasetMap()
|
||||
const requestHeaders = await headers()
|
||||
const nonce = IS_PROD ? (requestHeaders.get('x-nonce') ?? undefined) : undefined
|
||||
const themeProviderProps: Omit<ThemeProviderProps, 'children'> = {
|
||||
attribute: 'data-theme',
|
||||
defaultTheme: 'system',
|
||||
enableSystem: true,
|
||||
disableTransitionOnChange: true,
|
||||
}
|
||||
if (nonce !== undefined) themeProviderProps.nonce = nonce
|
||||
|
||||
const errorDocument = getAppAccessErrorDocument(requestHeaders)
|
||||
const [locale] = errorDocument
|
||||
@@ -79,7 +71,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
{!errorDocument && <CloudAnalytics />}
|
||||
<div className="isolate">
|
||||
<JotaiProvider>
|
||||
<ThemeProvider {...themeProviderProps}>
|
||||
<ThemeProvider nonce={nonce}>
|
||||
<NuqsAdapter>
|
||||
{errorDocument ? (
|
||||
children
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ vi.mock('@/service/console', async (importOriginal) => {
|
||||
}),
|
||||
}
|
||||
})
|
||||
vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }) }))
|
||||
vi.mock('@/features/theme/hooks', () => ({ useResolvedTheme: () => 'light' }))
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en_US',
|
||||
useDocLink: () => () => 'https://docs.example.com',
|
||||
|
||||
+4
-6
@@ -9,9 +9,8 @@ import { CollectionType } from '@/app/components/tools/types'
|
||||
import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { currentWorkspaceIdAtom } from '@/context/workspace-state'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { Theme } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import {
|
||||
createAgentToolProviderCatalog,
|
||||
@@ -45,7 +44,7 @@ function getProviderByTool(providerById: Map<string, ToolWithProvider>, tool: Pr
|
||||
export function useAgentPromptToolIconResolver(
|
||||
providerTypes?: ReadonlySet<ProviderTool['providerType']>,
|
||||
) {
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
|
||||
const { data: builtInTools } = useQuery(
|
||||
consoleQuery.workspaces.current.tools.builtin.get.queryOptions({
|
||||
@@ -87,8 +86,7 @@ export function useAgentPromptToolIconResolver(
|
||||
return useMemo(
|
||||
() => ({
|
||||
getProviderIcon: (provider: ToolWithProvider) => {
|
||||
const rawIcon =
|
||||
theme === Theme.dark && provider.icon_dark ? provider.icon_dark : provider.icon
|
||||
const rawIcon = theme === 'dark' && provider.icon_dark ? provider.icon_dark : provider.icon
|
||||
return normalizeProviderIcon(rawIcon, currentWorkspaceId)
|
||||
},
|
||||
getProviderIcons: (provider: ToolWithProvider) => ({
|
||||
@@ -98,7 +96,7 @@ export function useAgentPromptToolIconResolver(
|
||||
getConfiguredToolIcon: (tool: ProviderTool) => {
|
||||
const provider = getProviderByTool(providerById, tool)
|
||||
const rawIcon =
|
||||
theme === Theme.dark && (tool.iconDark ?? provider?.icon_dark)
|
||||
theme === 'dark' && (tool.iconDark ?? provider?.icon_dark)
|
||||
? (tool.iconDark ?? provider?.icon_dark)
|
||||
: getAgentProviderToolIcon(tool, provider)
|
||||
|
||||
|
||||
+9
-10
@@ -22,9 +22,8 @@ import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { getMarketplacePluginIconUrl } from '@/features/marketplace/api'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useResolvedTheme } from '@/features/theme/hooks'
|
||||
import { usePluginManifestInfo } from '@/service/use-plugins'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useAgentOrchestrateReadOnly } from '../../read-only-context'
|
||||
|
||||
function ProviderIcon({
|
||||
@@ -146,7 +145,7 @@ function UninstalledPluginStatus({
|
||||
return (
|
||||
<span className="flex h-6 shrink-0 items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 system-xs-medium text-components-button-secondary-text shadow-xs backdrop-blur-[5px]">
|
||||
{t(($) => $['detailPanel.toolSelector.uninstalledTitle'], { ns: 'plugin' })}
|
||||
<StatusDot className="ml-2" status="warning" />
|
||||
<StatusDot className="ms-2" status="warning" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -225,9 +224,9 @@ const ProviderToolActionItem = memo(
|
||||
}, [action.id, onRemoveAction])
|
||||
|
||||
return (
|
||||
<div className="group relative flex min-h-7 items-center gap-1 rounded-md py-px pr-0 pl-1 hover:bg-state-base-hover">
|
||||
<div className="absolute top-0 bottom-0 left-[13.5px] w-px bg-divider-regular" />
|
||||
<div className="flex min-w-0 flex-1 items-center py-1 pl-7">
|
||||
<div className="group relative flex min-h-7 items-center gap-1 rounded-md py-px ps-1 pe-0 hover:bg-state-base-hover">
|
||||
<div className="absolute inset-s-[13.5px] top-0 bottom-0 w-px bg-divider-regular" />
|
||||
<div className="flex min-w-0 flex-1 items-center py-1 ps-7">
|
||||
<span className="min-w-0 flex-1 truncate system-sm-regular text-text-secondary">
|
||||
{action.name}
|
||||
</span>
|
||||
@@ -299,14 +298,14 @@ export const AgentProviderToolItem = memo(
|
||||
}) => {
|
||||
const { t } = useTranslation(['agentV2'])
|
||||
const readOnly = useAgentOrchestrateReadOnly()
|
||||
const { theme } = useTheme()
|
||||
const theme = useResolvedTheme()
|
||||
const shouldFetchPluginManifest =
|
||||
isInstalled === false && !!tool.pluginId && !tool.pluginUniqueIdentifier
|
||||
const { data: pluginManifestData } = usePluginManifestInfo(
|
||||
shouldFetchPluginManifest ? tool.pluginId! : '',
|
||||
)
|
||||
const pluginManifest = pluginManifestData?.data.plugin
|
||||
const configuredIcon = theme === Theme.dark && tool.iconDark ? tool.iconDark : tool.icon
|
||||
const configuredIcon = theme === 'dark' && tool.iconDark ? tool.iconDark : tool.icon
|
||||
const icon =
|
||||
configuredIcon ??
|
||||
(pluginManifest && tool.pluginId ? getMarketplacePluginIconUrl(tool.pluginId) : undefined)
|
||||
@@ -322,8 +321,8 @@ export const AgentProviderToolItem = memo(
|
||||
onOpenChange={onOpenChange}
|
||||
className="overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-1 shadow-xs shadow-shadow-shadow-3"
|
||||
>
|
||||
<div className="flex min-h-7 items-center gap-1 rounded-lg py-0.5 pr-0.5 pl-1">
|
||||
<CollapsibleTrigger className="group/collapsible flex min-h-0 w-full min-w-0 flex-1 touch-manipulation items-center justify-start gap-2 rounded-md pr-1 text-left system-sm-medium text-text-secondary select-none">
|
||||
<div className="flex min-h-7 items-center gap-1 rounded-lg py-0.5 ps-1 pe-0.5">
|
||||
<CollapsibleTrigger className="group/collapsible flex min-h-0 w-full min-w-0 flex-1 touch-manipulation items-center justify-start gap-2 rounded-md pe-1 text-start system-sm-medium text-text-secondary select-none">
|
||||
<ProviderIcon icon={icon} iconClassName={tool.iconClassName} />
|
||||
<span className="flex min-w-0 items-center">
|
||||
<span className="min-w-0 truncate system-sm-medium text-text-primary">
|
||||
|
||||
@@ -133,7 +133,7 @@ it('keeps an embedded error language and its portaled menus local to the dialog'
|
||||
await user.click(screen.getByRole('button', { name: 'العربية (تونس)' }))
|
||||
expect(screen.getByRole('menu')).toHaveAttribute('dir', 'rtl')
|
||||
await user.keyboard('{Escape}')
|
||||
await user.click(screen.getByRole('button', { name: arCommon['theme.theme'] }))
|
||||
await user.click(screen.getByRole('button', { name: arCommon['theme.label'] }))
|
||||
expect(screen.getByRole('menu')).toHaveAttribute('dir', 'rtl')
|
||||
expect(document.documentElement.lang).toBe('zh-Hans')
|
||||
expect(document.documentElement.dir).toBe('ltr')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { ThemeProvider } from '@/features/theme/provider'
|
||||
import AppNotAccessible from './page'
|
||||
|
||||
const meta = {
|
||||
@@ -11,7 +11,7 @@ const meta = {
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThemeProvider attribute="data-theme" defaultTheme="light">
|
||||
<ThemeProvider defaultPreference="light">
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
|
||||
@@ -9,8 +9,8 @@ import { Suspense, useEffect, useState } from 'react'
|
||||
import { I18nextProvider, Trans, useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
|
||||
import ThemeSelector from '@/app/components/base/theme-selector'
|
||||
import { I18nDirectionProvider } from '@/app/components/provider/i18n-direction'
|
||||
import ThemeSelector from '@/features/theme/theme-selector'
|
||||
import { loadI18nResource } from '@/i18n/load-resource'
|
||||
import { getInitOptions } from '@/i18n/settings'
|
||||
import { usePathname, useSearchParams } from '@/next/navigation'
|
||||
@@ -60,7 +60,7 @@ function PageContent({ clientIp, embedded }: AppNotAccessibleProps) {
|
||||
)}
|
||||
>
|
||||
<div className="flex min-h-full min-w-0 flex-col rounded-lg border border-effects-highlight bg-background-default-subtle">
|
||||
<header className="flex shrink-0 items-center justify-between gap-2 pt-4 pr-3 pb-3 pl-6">
|
||||
<header className="flex shrink-0 items-center justify-between gap-2 ps-6 pe-3 pt-4 pb-3">
|
||||
<DifyLogo alt="Dify" size="large" className="w-15.75 shrink-0" />
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<LocaleMenu
|
||||
@@ -84,7 +84,7 @@ function PageContent({ clientIp, embedded }: AppNotAccessibleProps) {
|
||||
width={18}
|
||||
height={29.1429}
|
||||
alt=""
|
||||
className="absolute top-[21.43%] left-[12.5%] w-3/4 max-w-none"
|
||||
className="absolute inset-s-[12.5%] top-[21.43%] w-3/4 max-w-none"
|
||||
/>
|
||||
</span>
|
||||
<span className="px-2 system-sm-medium whitespace-nowrap">
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Theme
|
||||
|
||||
This feature owns the color theme contract. It is the only Web module that imports `next-themes`; lint rejects that import everywhere else.
|
||||
|
||||
- `theme.ts` owns the `ThemePreference` (`light`, `dark`, `system`) and `ResolvedTheme` (`light`, `dark`) types, and the option list (value, icon, `common` label key) that every theme control renders in the same order.
|
||||
- `provider.tsx` owns the provider configuration: the `data-theme` attribute, `system` as the default preference, and the CSP nonce.
|
||||
- `hooks.ts` separates the two questions a component can ask. `useThemePreference` returns what the user chose and is for theme controls. `useResolvedTheme` returns the light or dark value in effect and is for icons, editors, charts, and Marketplace links. A Marketplace link never receives `system`.
|
||||
- `theme-selector.tsx` is the standalone icon menu for pages without the account menu.
|
||||
|
||||
Theme copy lives in the `common` namespace (`theme.label`, `theme.light`, `theme.dark`, `theme.system`) because console, share, auth, and Goto Anything surfaces all load it. Surface-specific descriptions stay with their surface.
|
||||
+3
-3
@@ -18,18 +18,18 @@ describe('ThemeSelector', () => {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = renderToString(<ThemeSelector />)
|
||||
|
||||
expect(container.querySelector('button[aria-label="common.theme.theme"]')).not.toBeNull()
|
||||
expect(container.querySelector('button[aria-label="common.theme.label"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['common.theme.light', 'light'],
|
||||
['common.theme.dark', 'dark'],
|
||||
['common.theme.auto', 'system'],
|
||||
['common.theme.system', 'system'],
|
||||
])('selects %s', async (name, theme) => {
|
||||
const user = userEvent.setup()
|
||||
render(<ThemeSelector />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.theme.theme' }))
|
||||
await user.click(screen.getByRole('button', { name: 'common.theme.label' }))
|
||||
await user.click(await screen.findByRole('menuitemradio', { name }))
|
||||
|
||||
expect(setTheme).toHaveBeenCalledWith(theme)
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import type { ResolvedTheme, ThemePreference } from './theme'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback } from 'react'
|
||||
import { isThemePreference } from './theme'
|
||||
|
||||
type ThemePreferenceState = {
|
||||
preference: ThemePreference
|
||||
setPreference: (preference: ThemePreference) => void
|
||||
}
|
||||
|
||||
export function useThemePreference(): ThemePreferenceState {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const setPreference = useCallback(
|
||||
(preference: ThemePreference) => setTheme(preference),
|
||||
[setTheme],
|
||||
)
|
||||
|
||||
return {
|
||||
preference: isThemePreference(theme) ? theme : 'system',
|
||||
setPreference,
|
||||
}
|
||||
}
|
||||
|
||||
export function useResolvedTheme(): ResolvedTheme {
|
||||
const { resolvedTheme } = useTheme()
|
||||
return resolvedTheme === 'dark' ? 'dark' : 'light'
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ThemePreference } from './theme'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: ReactNode
|
||||
nonce?: string | undefined
|
||||
defaultPreference?: ThemePreference
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
nonce,
|
||||
defaultPreference = 'system',
|
||||
}: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="data-theme"
|
||||
defaultTheme={defaultPreference}
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
{...(nonce === undefined ? {} : { nonce })}
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import type { ThemePreference } from './theme'
|
||||
import { useDirection } from '@langgenius/dify-ui/direction-provider'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import {
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuRadioGroup,
|
||||
MenuRadioItem,
|
||||
MenuRadioItemIndicator,
|
||||
MenuTrigger,
|
||||
} from '@langgenius/dify-ui/menu'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useThemePreference } from './hooks'
|
||||
import { themeOptions } from './theme'
|
||||
|
||||
const subscribeToNothing = () => () => {}
|
||||
|
||||
const useIsHydrated = () =>
|
||||
useSyncExternalStore(
|
||||
subscribeToNothing,
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
|
||||
export default function ThemeSelector() {
|
||||
const direction = useDirection()
|
||||
const { t } = useTranslation(['common'])
|
||||
const { preference, setPreference } = useThemePreference()
|
||||
const isHydrated = useIsHydrated()
|
||||
const currentOption = themeOptions.find((option) => option.value === preference)
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger
|
||||
render={
|
||||
<IconButton
|
||||
aria-label={t(($) => $['theme.label'], { ns: 'common' })}
|
||||
size="lg"
|
||||
className="data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
{isHydrated && currentOption ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className={`${currentOption.iconClassName} size-4 text-text-tertiary`}
|
||||
/>
|
||||
) : (
|
||||
<span aria-hidden className="size-4" />
|
||||
)}
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<MenuContent dir={direction} sideOffset={6} className="w-36">
|
||||
<MenuRadioGroup<ThemePreference> value={preference} onValueChange={setPreference}>
|
||||
{themeOptions.map((option) => (
|
||||
<MenuRadioItem<ThemePreference> key={option.value} value={option.value} closeOnClick>
|
||||
<span className={`${option.iconClassName} size-4 text-text-tertiary`} />
|
||||
<span className="grow px-1 system-md-regular">
|
||||
{t(($) => $[option.labelKey], { ns: 'common' })}
|
||||
</span>
|
||||
<MenuRadioItemIndicator data-testid={`${option.value}-icon`} />
|
||||
</MenuRadioItem>
|
||||
))}
|
||||
</MenuRadioGroup>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type ThemePreference = 'light' | 'dark' | 'system'
|
||||
export type ResolvedTheme = Exclude<ThemePreference, 'system'>
|
||||
|
||||
export const themeOptions = [
|
||||
{ value: 'light', iconClassName: 'i-ri-sun-line', labelKey: 'theme.light' },
|
||||
{ value: 'dark', iconClassName: 'i-ri-moon-line', labelKey: 'theme.dark' },
|
||||
{ value: 'system', iconClassName: 'i-ri-computer-line', labelKey: 'theme.system' },
|
||||
] as const satisfies readonly {
|
||||
value: ThemePreference
|
||||
iconClassName: string
|
||||
labelKey: `theme.${ThemePreference}`
|
||||
}[]
|
||||
|
||||
export const isThemePreference = (value: unknown): value is ThemePreference =>
|
||||
themeOptions.some((option) => option.value === value)
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useTheme as useBaseTheme } from 'next-themes'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
const useTheme = () => {
|
||||
const { theme, resolvedTheme, ...rest } = useBaseTheme()
|
||||
return {
|
||||
// only returns 'light' or 'dark' theme
|
||||
theme: theme === Theme.system ? (resolvedTheme as Theme) : (theme as Theme),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export default useTheme
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"account.account": "الحساب",
|
||||
"account.appearanceDark": "داكن",
|
||||
"account.appearanceFollowSystem": "Follow system",
|
||||
"account.appearanceLabel": "المظهر",
|
||||
"account.appearanceLight": "فاتح",
|
||||
"account.appearanceSystem": "النظام",
|
||||
"account.changeEmail.authTip": "بمجرد تغيير بريدك الإلكتروني، لن تتمكن حسابات Google أو GitHub المرتبطة ببريدك الإلكتروني القديم من تسجيل الدخول إلى هذا الحساب.",
|
||||
"account.changeEmail.changeTo": "تغيير إلى {{email}}",
|
||||
"account.changeEmail.codeLabel": "رمز التحقق",
|
||||
|
||||
@@ -71,11 +71,8 @@
|
||||
"gotoAnything.actions.slashDesc": "تنفيذ الأوامر (اكتب / لرؤية جميع الأوامر المتاحة)",
|
||||
"gotoAnything.actions.slashTitle": "الأوامر",
|
||||
"gotoAnything.actions.themeCategoryDesc": "تغيير السمة",
|
||||
"gotoAnything.actions.themeDark": "السمة الداكنة",
|
||||
"gotoAnything.actions.themeDarkDesc": "استخدم المظهر الداكن",
|
||||
"gotoAnything.actions.themeLight": "السمة الفاتحة",
|
||||
"gotoAnything.actions.themeLightDesc": "استخدم المظهر الفاتح",
|
||||
"gotoAnything.actions.themeSystem": "سمة النظام",
|
||||
"gotoAnything.actions.themeSystemDesc": "اتبع مظهر نظام التشغيل",
|
||||
"gotoAnything.contextTokenHint": "عندما يكون الحقل فارغًا، يزيل Backspace عامل التصفية أو الصفحة الحالية.",
|
||||
"gotoAnything.emptyState.noAppsFound": "لم يتم العثور على تطبيقات",
|
||||
|
||||
@@ -326,10 +326,10 @@
|
||||
"tag.placeholder": "العلامات",
|
||||
"tag.selectorPlaceholder": "اكتب للبحث أو الإنشاء",
|
||||
"tag.tags": "العلامات",
|
||||
"theme.auto": "النظام",
|
||||
"theme.dark": "داكن",
|
||||
"theme.label": "المظهر",
|
||||
"theme.light": "فاتح",
|
||||
"theme.theme": "السمة",
|
||||
"theme.system": "النظام",
|
||||
"triggerPage.description": "حوّل أحداث الجهات الخارجية إلى مدخلات يمكن لتطبيقاتك التعرف عليها والتصرف بناءً عليها — لأتمتة workflows بدون تشغيل يدوي.",
|
||||
"unit.char": "أحرف",
|
||||
"userProfile.about": "حول",
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"account.account": "Hesab",
|
||||
"account.appearanceDark": "Tünd",
|
||||
"account.appearanceFollowSystem": "Sistemə uyğunlaşdır",
|
||||
"account.appearanceLabel": "Görünüş",
|
||||
"account.appearanceLight": "Açıq",
|
||||
"account.appearanceSystem": "Sistem",
|
||||
"account.changeEmail.authTip": "E-poçt ünvanınız dəyişdirildikdən sonra köhnə e-poçt ünvanınıza bağlı Google və ya GitHub hesabları ilə bu hesaba daxil olmaq mümkün olmayacaq.",
|
||||
"account.changeEmail.changeTo": "{{email}} ünvanı ilə əvəz et",
|
||||
"account.changeEmail.codeLabel": "Doğrulama kodu",
|
||||
|
||||
@@ -71,11 +71,8 @@
|
||||
"gotoAnything.actions.slashDesc": "Əmrləri icra et (bütün mövcud əmrləri görmək üçün / yaz)",
|
||||
"gotoAnything.actions.slashTitle": "Əmrlər",
|
||||
"gotoAnything.actions.themeCategoryDesc": "Mövzunu dəyişin",
|
||||
"gotoAnything.actions.themeDark": "Tünd temaya keç",
|
||||
"gotoAnything.actions.themeDarkDesc": "Tünd görünüşdən istifadə et",
|
||||
"gotoAnything.actions.themeLight": "Açıq temaya keç",
|
||||
"gotoAnything.actions.themeLightDesc": "Açıq görünüşdən istifadə et",
|
||||
"gotoAnything.actions.themeSystem": "Sistem temasına keç",
|
||||
"gotoAnything.actions.themeSystemDesc": "Əməliyyat sisteminizin görünüşünə uyğunlaşdır",
|
||||
"gotoAnything.contextTokenHint": "Sahə boş olduqda Backspace cari filtri və ya səhifəni silir.",
|
||||
"gotoAnything.emptyState.noAppsFound": "Tətbiq tapılmadı",
|
||||
|
||||
@@ -326,10 +326,10 @@
|
||||
"tag.placeholder": "Etiketlər",
|
||||
"tag.selectorPlaceholder": "Axtarmaq və ya yaratmaq üçün yazın",
|
||||
"tag.tags": "Etiketlər",
|
||||
"theme.auto": "Sistem",
|
||||
"theme.dark": "Tünd",
|
||||
"theme.label": "Görünüş",
|
||||
"theme.light": "Açıq",
|
||||
"theme.theme": "Tema",
|
||||
"theme.system": "Sistem",
|
||||
"triggerPage.description": "Üçüncü tərəf hadisələrini tətbiqlərinizin tanıyıb emal edə biləcəyi girişlərə çevirin, iş axınlarını əl ilə işə salmadan avtomatlaşdırın.",
|
||||
"unit.char": "simvol",
|
||||
"userProfile.about": "Haqqında",
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"account.account": "Konto",
|
||||
"account.appearanceDark": "Dunkel",
|
||||
"account.appearanceFollowSystem": "Follow system",
|
||||
"account.appearanceLabel": "Darstellung",
|
||||
"account.appearanceLight": "Hell",
|
||||
"account.appearanceSystem": "System",
|
||||
"account.changeEmail.authTip": "Sobald Ihre E-Mail geändert wurde, können Google- oder GitHub-Konten, die mit Ihrer alten E-Mail verknüpft sind, nicht mehr auf dieses Konto zugreifen.",
|
||||
"account.changeEmail.changeTo": "Ändern zu {{email}}",
|
||||
"account.changeEmail.codeLabel": "Bestätigungscode",
|
||||
|
||||
@@ -71,11 +71,8 @@
|
||||
"gotoAnything.actions.slashDesc": "Führen Sie Befehle wie /theme, /lang aus",
|
||||
"gotoAnything.actions.slashTitle": "Befehle",
|
||||
"gotoAnything.actions.themeCategoryDesc": "Design wechseln",
|
||||
"gotoAnything.actions.themeDark": "Dunkles Thema",
|
||||
"gotoAnything.actions.themeDarkDesc": "Verwenden Sie das dunkle Erscheinungsbild",
|
||||
"gotoAnything.actions.themeLight": "Helles Design",
|
||||
"gotoAnything.actions.themeLightDesc": "Verwenden Sie das helle Erscheinungsbild",
|
||||
"gotoAnything.actions.themeSystem": "Systemthema",
|
||||
"gotoAnything.actions.themeSystemDesc": "Folgen Sie dem Aussehen Ihres Betriebssystems",
|
||||
"gotoAnything.contextTokenHint": "Bei leerem Feld entfernt Backspace den aktuellen Filter oder die Seite.",
|
||||
"gotoAnything.emptyState.noAppsFound": "Keine Apps gefunden",
|
||||
|
||||
@@ -326,10 +326,10 @@
|
||||
"tag.placeholder": "Tags",
|
||||
"tag.selectorPlaceholder": "Typ zum Suchen oder Erstellen",
|
||||
"tag.tags": "Tags",
|
||||
"theme.auto": "System",
|
||||
"theme.dark": "dunkel",
|
||||
"theme.light": "Licht",
|
||||
"theme.theme": "Thema",
|
||||
"theme.dark": "Dunkel",
|
||||
"theme.label": "Darstellung",
|
||||
"theme.light": "Hell",
|
||||
"theme.system": "System",
|
||||
"triggerPage.description": "Wandle Drittanbieter-Ereignisse in Eingaben um, die deine Apps erkennen und verarbeiten können — automatisiere Workflows ohne manuelle Auslöser.",
|
||||
"unit.char": "Zeichen",
|
||||
"userProfile.about": "Über",
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"account.account": "Account",
|
||||
"account.appearanceDark": "Dark",
|
||||
"account.appearanceFollowSystem": "Follow system",
|
||||
"account.appearanceLabel": "Appearance",
|
||||
"account.appearanceLight": "Light",
|
||||
"account.appearanceSystem": "System",
|
||||
"account.changeEmail.authTip": "Once your email is changed, Google or GitHub accounts linked to your old email will no longer be able to log in to this account.",
|
||||
"account.changeEmail.changeTo": "Change to {{email}}",
|
||||
"account.changeEmail.codeLabel": "Verification code",
|
||||
|
||||
@@ -70,12 +70,9 @@
|
||||
"gotoAnything.actions.searchApplications": "Search Applications",
|
||||
"gotoAnything.actions.slashDesc": "Execute commands (type / to see all available commands)",
|
||||
"gotoAnything.actions.slashTitle": "Commands",
|
||||
"gotoAnything.actions.themeCategoryDesc": "Switch theme",
|
||||
"gotoAnything.actions.themeDark": "Dark Theme",
|
||||
"gotoAnything.actions.themeCategoryDesc": "Change appearance",
|
||||
"gotoAnything.actions.themeDarkDesc": "Use dark appearance",
|
||||
"gotoAnything.actions.themeLight": "Light Theme",
|
||||
"gotoAnything.actions.themeLightDesc": "Use light appearance",
|
||||
"gotoAnything.actions.themeSystem": "System Theme",
|
||||
"gotoAnything.actions.themeSystemDesc": "Follow your OS appearance",
|
||||
"gotoAnything.contextTokenHint": "In an empty field, Backspace removes the current filter or page.",
|
||||
"gotoAnything.emptyState.noAppsFound": "No apps found",
|
||||
|
||||
Loaded 100 of 166 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user