Compare commits
88 Commits
copilot/ch
...
feat/ui-on
| Author | SHA1 | Date | |
|---|---|---|---|
| b70241ad36 | |||
| 4abe622b2e | |||
| 16c32c82e3 | |||
| 46424513d1 | |||
| 2c4baa20d8 | |||
| b0ae553f2e | |||
| 0266a12ee5 | |||
| 9d7765d5fd | |||
| d4ef983f42 | |||
| 018f36711d | |||
| dacd333e4a | |||
| b079a26314 | |||
| 7e953ebe0b | |||
| b4d28fca54 | |||
| 728c6b8201 | |||
| f56e23b5fd | |||
| 5600cefa53 | |||
| 561eb9cbd2 | |||
| 83766ca694 | |||
| 678be94d22 | |||
| 9e852429be | |||
| d93c5028f1 | |||
| 54f189305e | |||
| a610a24507 | |||
| 05e8a94bb5 | |||
| b2e2e7b60b | |||
| e7d2e66ff5 | |||
| c51069685c | |||
| 28c208f36a | |||
| 53a1386b87 | |||
| 0e366c7300 | |||
| 939bdde373 | |||
| 13dfa3aba4 | |||
| 2705a7c1db | |||
| 258a751b8c | |||
| 5a35d3d9cd | |||
| c3fbafae83 | |||
| f727c8f838 | |||
| 90af4c39b4 | |||
| f7c3a4e4cb | |||
| be7d043edd | |||
| cef8fe3a4b | |||
| afe0e6c393 | |||
| 37309b931e | |||
| 6a83c6705c | |||
| 3e75d5e443 | |||
| 7be8a5b883 | |||
| 80dcb344f4 | |||
| b029c9b1cd | |||
| 6cb97e9201 | |||
| 4ef2e952bd | |||
| cc5545339c | |||
| 0a8c46a3a7 | |||
| 65770903d1 | |||
| 5a6ba2ffb5 | |||
| aa53afe07d | |||
| 4740a89f4a | |||
| 328db3d67a | |||
| 88062fb247 | |||
| 045da59220 | |||
| 948b0f6bc7 | |||
| 14a59f6e44 | |||
| f9f361113e | |||
| eea6f59307 | |||
| 718f69dc43 | |||
| 82a2ba9264 | |||
| 6c8e032fbb | |||
| 28c2c3bfd3 | |||
| 9d463e1024 | |||
| 7f87616625 | |||
| 43a04ed0c2 | |||
| 5083edd0ce | |||
| 8306fa41b9 | |||
| 8f33305e90 | |||
| 7077a43c1c | |||
| 884a43ae0a | |||
| 914f89f478 | |||
| 163153db18 | |||
| 49d890d514 | |||
| 0292bc2728 | |||
| 5c21120977 | |||
| 2d5186fb28 | |||
| 06f076e0ff | |||
| 5b79f7e99d | |||
| 1cee1a25b6 | |||
| c0f237bf35 | |||
| 75d7fc0526 | |||
| c057b5c5ff |
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: frontend-code-review
|
||||
description: "Trigger when the user requests a review of frontend files (e.g., `.tsx`, `.ts`, `.js`). Support both pending-change reviews and focused file reviews while applying the checklist rules."
|
||||
description: "Trigger when the user requests a review of frontend files (e.g., `.tsx`, `.ts`, `.js`). Support pending-change and focused file reviews while applying checklist rules, shared component reuse checks, and React component structure guidance from how-to-write-component."
|
||||
---
|
||||
|
||||
# Frontend Code Review
|
||||
@ -16,10 +16,12 @@ Stick to the checklist below for every applicable file and mode.
|
||||
## Checklist
|
||||
See [references/code-quality.md](references/code-quality.md), [references/performance.md](references/performance.md), [references/business-logic.md](references/business-logic.md) for the living checklist split by category—treat it as the canonical set of rules to follow.
|
||||
|
||||
When reviewing React/TypeScript components, also apply the repo-local `how-to-write-component` skill as the component architecture checklist. In particular, check ownership boundaries, props and API types, query/mutation usage, navigation choices, effect usage, unnecessary wrappers, and unnecessary memoization.
|
||||
|
||||
Flag each rule violation with urgency metadata so future reviewers can prioritize fixes.
|
||||
|
||||
## Review Process
|
||||
1. Open the relevant component/module. Gather lines that relate to class names, React Flow hooks, prop memoization, and styling.
|
||||
1. Open the relevant component/module. Gather lines that relate to shared base/dify-ui component reuse, class names, styling/CSS imports, file size and component boundaries, i18n keys, behavior-sensitive UI interactions, React Flow hooks, and prop memoization.
|
||||
2. For each rule in the review point, note where the code deviates and capture a representative snippet.
|
||||
3. Compose the review section per the template below. Group violations first by **Urgent** flag, then by category order (Code Quality, Performance, Business Logic).
|
||||
|
||||
@ -70,4 +72,3 @@ If you use Template A (i.e., there are issues to fix) and at least one issue req
|
||||
## Code review
|
||||
No issues found.
|
||||
```
|
||||
|
||||
|
||||
@ -13,3 +13,29 @@ Node components are also used when creating a RAG Pipe from a template, but in t
|
||||
### Suggested Fix
|
||||
|
||||
Use `import { useNodes } from 'reactflow'` instead of `import useNodes from '@/app/components/workflow/store/workflow/use-nodes'`.
|
||||
|
||||
## Locale keys must be complete
|
||||
|
||||
IsUrgent: True
|
||||
Category: Business Logic
|
||||
|
||||
### Description
|
||||
|
||||
When adding or changing user-facing i18n keys, ensure every supported locale file has the same key set as `web/i18n/en-US/`. Do not add only English keys or only a partial subset of locales; `pnpm i18n:check --file <name>` should pass for the touched translation file.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
Add matching keys to every existing supported locale file for the touched translation namespace, keeping key paths aligned with the English entry.
|
||||
|
||||
## Preserve behavior-sensitive interactions
|
||||
|
||||
IsUrgent: True
|
||||
Category: Business Logic
|
||||
|
||||
### Description
|
||||
|
||||
When changing existing navigation, sidebar, dropdown, webapp list, or app-switching UI, compare behavior against the existing implementation before approving the change. Watch for regressions in expand/collapse arrows, hover persistence, pin/delete controls, routing, keyboard/focus handling, and open-state ownership.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
Reuse or extend the existing component when it already owns the interaction logic. If a refactor is needed, preserve the old interaction contract and add or update focused tests for the changed behavior.
|
||||
|
||||
@ -7,12 +7,12 @@ Category: Code Quality
|
||||
|
||||
### Description
|
||||
|
||||
Ensure conditional CSS is handled via the shared `classNames` instead of custom ternaries, string concatenation, or template strings. Centralizing class logic keeps components consistent and easier to maintain.
|
||||
Ensure conditional CSS and multi-line class composition are handled via the shared `cn` helper instead of custom ternaries, string concatenation, array `.join(' ')`, or template strings. Centralizing class logic keeps components consistent and easier to maintain.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
```ts
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
const classNames = cn(isActive ? 'text-primary-600' : 'text-gray-500')
|
||||
```
|
||||
|
||||
@ -25,7 +25,34 @@ Category: Code Quality
|
||||
|
||||
Favor Tailwind CSS utility classes instead of adding new `.module.css` files unless a Tailwind combination cannot achieve the required styling. Keeping styles in Tailwind improves consistency and reduces maintenance overhead.
|
||||
|
||||
Update this file when adding, editing, or removing Code Quality rules so the catalog remains accurate.
|
||||
## CSS files must be scoped
|
||||
|
||||
IsUrgent: True
|
||||
Category: Code Quality
|
||||
|
||||
### Description
|
||||
|
||||
When CSS is truly necessary, use component-scoped `*.module.css`. Do not add component-level CSS through plain `.css` files, and do not import component CSS from `globals.css`; both patterns risk style leakage across the app.
|
||||
|
||||
## Split oversized components cautiously
|
||||
|
||||
Category: Code Quality
|
||||
|
||||
### Description
|
||||
|
||||
When a frontend file grows large or mixes multiple responsibilities, suggest splitting it into focused components, hooks, or utilities. Prefer shallow local structure that matches existing repo patterns, such as a sibling `components/` folder, and avoid deep folder hierarchies unless the surrounding code already uses them.
|
||||
|
||||
## Reuse base and dify-ui components before hand-rolling UI
|
||||
|
||||
Category: Code Quality
|
||||
|
||||
### Description
|
||||
|
||||
Before approving new or modified frontend UI, check whether the code manually recreates behavior or styling already owned by `@langgenius/dify-ui/*` or `web/app/components/base/*`. Common examples include `Button`, `Input`, `ToggleGroup`, `Popover`, `DropdownMenu`, `AlertDialog`, `Switch`, `Avatar`, `ScrollArea`, `toast`, and existing feature components. Prefer composing existing primitives instead of duplicating borders, focus states, disabled states, segmented controls, inputs, overlays, or buttons.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
Replace hand-written UI chrome with the nearest shared primitive, keeping feature-specific layout, state ownership, labels, and workflow behavior local.
|
||||
|
||||
## Classname ordering for easy overrides
|
||||
|
||||
@ -36,9 +63,11 @@ When writing components, always place the incoming `className` prop after the co
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
|
||||
const Button = ({ className }) => {
|
||||
return <div className={cn('bg-primary-600', className)}></div>
|
||||
}
|
||||
```
|
||||
|
||||
Update this file when adding, editing, or removing Code Quality rules so the catalog remains accurate.
|
||||
|
||||
@ -43,3 +43,14 @@ const config = useMemo(() => ({
|
||||
config={config}
|
||||
/>
|
||||
```
|
||||
|
||||
## Custom SVG icon generation
|
||||
|
||||
IsUrgent: False
|
||||
Category: Performance
|
||||
|
||||
### Description
|
||||
|
||||
New custom SVG icons should be added to `packages/iconify-collections/assets/...`, generated with `pnpm --filter @dify/iconify-collections generate`, checked with `pnpm --filter @dify/iconify-collections check:dimensions`, and consumed through Tailwind `i-custom-*` classes. Do not add new generated React icon components or JSON files under `web/app/components/base/icons/src/...` for new custom SVG icons.
|
||||
|
||||
When reviewing generated `packages/iconify-collections/custom-*/icons.json` diffs, verify unrelated existing icons did not lose or change intrinsic `width` / `height`.
|
||||
33
.agents/skills/karpathy-guidelines/SKILL.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Lightweight coding guardrails for making focused, simple, and verifiable changes in this repo. Use for all coding work.
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Use this skill whenever you touch code in this repository.
|
||||
|
||||
## Principles
|
||||
|
||||
- Keep the change small and directly tied to the user request.
|
||||
- Prefer the simplest implementation that fits the existing codebase.
|
||||
- Read the nearby code first, then match its patterns.
|
||||
- Avoid unrelated refactors, broad rewrites, or style churn.
|
||||
- Preserve existing behavior unless the user explicitly asked to change it.
|
||||
- Treat regressions as a signal to narrow the change, not to add workaround layers.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the current implementation and tests around the change.
|
||||
2. Make the smallest coherent edit.
|
||||
3. Add or update focused tests when the behavior changes or the risk is non-trivial.
|
||||
4. Run the narrowest relevant verification first.
|
||||
5. Report exactly what was verified and anything left unverified.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Does this change solve the stated problem without expanding scope?
|
||||
- Did it preserve existing route/component/data-flow semantics?
|
||||
- Are new abstractions justified by real complexity?
|
||||
- Are tests focused on the behavior that could regress?
|
||||
- Are unrelated files and generated artifacts left alone?
|
||||
73
.github/scripts/check-hotfix-cherry-picks.sh
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_SHA=${BASE_SHA:-}
|
||||
HEAD_SHA=${HEAD_SHA:-}
|
||||
MAIN_REF=${MAIN_REF:-origin/main}
|
||||
REMEDIATION_HINT="Changes should be made from the main branch using git cherry-pick -x."
|
||||
|
||||
error() {
|
||||
printf 'ERROR: %s\n' "$1" >&2
|
||||
}
|
||||
|
||||
if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then
|
||||
error "BASE_SHA and HEAD_SHA are required. $REMEDIATION_HINT"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify "$BASE_SHA^{commit}" > /dev/null 2>&1; then
|
||||
error "Base commit '$BASE_SHA' is not available in the local git checkout."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify "$HEAD_SHA^{commit}" > /dev/null 2>&1; then
|
||||
error "Head commit '$HEAD_SHA' is not available in the local git checkout."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify "$MAIN_REF^{commit}" > /dev/null 2>&1; then
|
||||
error "Main ref '$MAIN_REF' is not available in the local git checkout. $REMEDIATION_HINT"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
failed=0
|
||||
checked=0
|
||||
|
||||
while IFS= read -r commit_sha; do
|
||||
[[ -n "$commit_sha" ]] || continue
|
||||
|
||||
checked=$((checked + 1))
|
||||
subject=$(git log -1 --format=%s "$commit_sha")
|
||||
source_sha=$(
|
||||
git log -1 --format=%B "$commit_sha" \
|
||||
| sed -nE 's/^\(cherry picked from commit ([0-9a-fA-F]{7,64})\)$/\1/p' \
|
||||
| tail -n 1
|
||||
)
|
||||
|
||||
if [[ -z "$source_sha" ]]; then
|
||||
error "Commit $commit_sha ($subject) is missing cherry-pick provenance. $REMEDIATION_HINT"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! git cat-file -e "$source_sha^{commit}" 2> /dev/null; then
|
||||
error "Commit $commit_sha ($subject) references source $source_sha, but that commit is not available locally. $REMEDIATION_HINT"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! git merge-base --is-ancestor "$source_sha" "$MAIN_REF"; then
|
||||
error "Commit $commit_sha ($subject) references source $source_sha, but that source is not reachable from main ($MAIN_REF). $REMEDIATION_HINT"
|
||||
failed=1
|
||||
fi
|
||||
done < <(git rev-list --reverse "$BASE_SHA..$HEAD_SHA")
|
||||
|
||||
if [[ "$failed" -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$checked" -eq 0 ]]; then
|
||||
echo "No PR commits to check."
|
||||
else
|
||||
echo "Verified $checked PR commit(s) include cherry-pick provenance from main."
|
||||
fi
|
||||
49
.github/workflows/hotfix-cherry-pick.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Hotfix Cherry-Pick Provenance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'hotfix/**'
|
||||
- 'lts/**'
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- ready_for_review
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: hotfix-cherry-pick-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-cherry-pick-provenance:
|
||||
name: Require cherry-pick provenance
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch PR base, PR head, and main
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
git fetch --no-tags --prune origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main" \
|
||||
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" \
|
||||
"+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head"
|
||||
|
||||
- name: Load checker from main
|
||||
run: git show origin/main:.github/scripts/check-hotfix-cherry-picks.sh > "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
|
||||
|
||||
- name: Check PR commits
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
MAIN_REF: origin/main
|
||||
run: bash "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
|
||||
4
MOCKS_TO_REMOVE_BEFORE_RELEASE.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Mocks to Remove Before Release
|
||||
|
||||
- `emptyAppList=true`: frontend URL preview flag for forcing the `/apps` page into the first-empty state. Remove the parser and rendering override before release.
|
||||
- `emptyDataList=true`: frontend URL preview flag for forcing the `/datasets` page into the first-empty state. Remove the parser and rendering override before release.
|
||||
@ -874,6 +874,7 @@ class ToolBuiltinProviderSetDefaultApi(Resource):
|
||||
@console_ns.expect(console_ns.models[BuiltinProviderDefaultCredentialPayload.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@account_initialization_required
|
||||
def post(self, provider):
|
||||
_, current_tenant_id = current_account_with_tenant()
|
||||
|
||||
@ -16,6 +16,7 @@ from pydantic import TypeAdapter
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from configs import dify_config
|
||||
from core.agent.entities import AgentToolEntity
|
||||
from core.helper import marketplace
|
||||
from core.plugin.entities.plugin import PluginInstallationSource
|
||||
@ -310,6 +311,8 @@ class PluginMigration:
|
||||
"""
|
||||
Fetch plugin unique identifier using plugin id.
|
||||
"""
|
||||
if not dify_config.MARKETPLACE_ENABLED:
|
||||
return None
|
||||
plugin_manifest = marketplace.batch_fetch_plugin_manifests([plugin_id])
|
||||
if not plugin_manifest:
|
||||
return None
|
||||
@ -542,6 +545,11 @@ class PluginMigration:
|
||||
"""
|
||||
Install plugins for a tenant.
|
||||
"""
|
||||
if plugin_identifiers_map and not dify_config.MARKETPLACE_ENABLED:
|
||||
raise ValueError(
|
||||
"Marketplace disabled in offline mode; cannot bulk-install plugins. "
|
||||
"Pre-upload plugin packages via Console first."
|
||||
)
|
||||
manager = PluginInstaller()
|
||||
|
||||
# download all the plugins and upload
|
||||
|
||||
@ -73,35 +73,43 @@ class PluginService:
|
||||
cache_not_exists.append(plugin_id)
|
||||
|
||||
if cache_not_exists:
|
||||
manifests = {
|
||||
manifest.plugin_id: manifest
|
||||
for manifest in marketplace.batch_fetch_plugin_manifests(cache_not_exists)
|
||||
}
|
||||
|
||||
for plugin_id, manifest in manifests.items():
|
||||
latest_plugin = PluginService.LatestPluginCache(
|
||||
plugin_id=plugin_id,
|
||||
version=manifest.latest_version,
|
||||
unique_identifier=manifest.latest_package_identifier,
|
||||
status=manifest.status,
|
||||
deprecated_reason=manifest.deprecated_reason,
|
||||
alternative_plugin_id=manifest.alternative_plugin_id,
|
||||
if not dify_config.MARKETPLACE_ENABLED:
|
||||
logger.info(
|
||||
"Marketplace disabled; skipping latest-plugins metadata fetch for %d ids",
|
||||
len(cache_not_exists),
|
||||
)
|
||||
for plugin_id in cache_not_exists:
|
||||
result[plugin_id] = None
|
||||
else:
|
||||
manifests = {
|
||||
manifest.plugin_id: manifest
|
||||
for manifest in marketplace.batch_fetch_plugin_manifests(cache_not_exists)
|
||||
}
|
||||
|
||||
# Store in Redis
|
||||
redis_client.setex(
|
||||
f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}",
|
||||
PluginService.REDIS_TTL,
|
||||
latest_plugin.model_dump_json(),
|
||||
)
|
||||
for plugin_id, manifest in manifests.items():
|
||||
latest_plugin = PluginService.LatestPluginCache(
|
||||
plugin_id=plugin_id,
|
||||
version=manifest.latest_version,
|
||||
unique_identifier=manifest.latest_package_identifier,
|
||||
status=manifest.status,
|
||||
deprecated_reason=manifest.deprecated_reason,
|
||||
alternative_plugin_id=manifest.alternative_plugin_id,
|
||||
)
|
||||
|
||||
result[plugin_id] = latest_plugin
|
||||
# Store in Redis
|
||||
redis_client.setex(
|
||||
f"{PluginService.REDIS_KEY_PREFIX}{plugin_id}",
|
||||
PluginService.REDIS_TTL,
|
||||
latest_plugin.model_dump_json(),
|
||||
)
|
||||
|
||||
# pop plugin_id from cache_not_exists
|
||||
cache_not_exists.remove(plugin_id)
|
||||
result[plugin_id] = latest_plugin
|
||||
|
||||
for plugin_id in cache_not_exists:
|
||||
result[plugin_id] = None
|
||||
# pop plugin_id from cache_not_exists
|
||||
cache_not_exists.remove(plugin_id)
|
||||
|
||||
for plugin_id in cache_not_exists:
|
||||
result[plugin_id] = None
|
||||
|
||||
return result
|
||||
except Exception:
|
||||
|
||||
@ -1350,6 +1350,12 @@ class RagPipelineService:
|
||||
)
|
||||
return workflow_node_execution_db_model
|
||||
|
||||
def _fetch_recommended_plugin_manifests(self, plugin_ids: list[str]) -> list[Any]:
|
||||
if not dify_config.MARKETPLACE_ENABLED:
|
||||
logger.info("Marketplace disabled; recommended-plugins list empty")
|
||||
return []
|
||||
return marketplace.batch_fetch_plugin_by_ids(plugin_ids)
|
||||
|
||||
def get_recommended_plugins(self, type: str) -> dict[str, Any]:
|
||||
# Query active recommended plugins
|
||||
stmt = select(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
|
||||
@ -1372,7 +1378,7 @@ class RagPipelineService:
|
||||
)
|
||||
providers_map = {provider.plugin_id: provider.to_dict() for provider in providers}
|
||||
|
||||
plugin_manifests = marketplace.batch_fetch_plugin_by_ids(plugin_ids)
|
||||
plugin_manifests = self._fetch_recommended_plugin_manifests(plugin_ids)
|
||||
plugin_manifests_map = {manifest["plugin_id"]: manifest for manifest in plugin_manifests}
|
||||
|
||||
installed_plugin_list = []
|
||||
|
||||
@ -9,6 +9,7 @@ import yaml
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import select
|
||||
|
||||
from configs import dify_config
|
||||
from constants import DOCUMENT_EXTENSIONS
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
|
||||
@ -273,6 +274,13 @@ class RagPipelineTransformService:
|
||||
plugin_unique_identifier = dependency.get("value", {}).get("plugin_unique_identifier")
|
||||
plugin_id = plugin_unique_identifier.split(":")[0]
|
||||
if plugin_id not in installed_plugins_ids:
|
||||
if not dify_config.MARKETPLACE_ENABLED:
|
||||
logger.warning(
|
||||
"Marketplace disabled; skipping auto-install of %s. "
|
||||
"Pre-install via Console if pipeline requires it.",
|
||||
plugin_id,
|
||||
)
|
||||
continue
|
||||
plugin_unique_identifier = plugin_migration._fetch_plugin_unique_identifier(plugin_id) # type: ignore
|
||||
if plugin_unique_identifier:
|
||||
need_install_plugin_unique_identifiers.append(plugin_unique_identifier)
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from services.plugin.plugin_migration import PluginMigration
|
||||
|
||||
MIGRATION_MODULE = "services.plugin.plugin_migration"
|
||||
|
||||
|
||||
def test_fetch_plugin_unique_identifier_returns_none_when_disabled(mocker: MockerFixture) -> None:
|
||||
mocker.patch("services.plugin.plugin_migration.dify_config.MARKETPLACE_ENABLED", False)
|
||||
batch_fetch = mocker.patch("services.plugin.plugin_migration.marketplace.batch_fetch_plugin_manifests")
|
||||
|
||||
result = PluginMigration._fetch_plugin_unique_identifier("langgenius/openai")
|
||||
|
||||
assert result is None
|
||||
batch_fetch.assert_not_called()
|
||||
|
||||
|
||||
def test_fetch_plugin_unique_identifier_calls_marketplace_when_enabled(mocker: MockerFixture) -> None:
|
||||
mocker.patch("services.plugin.plugin_migration.dify_config.MARKETPLACE_ENABLED", True)
|
||||
manifest = mocker.MagicMock()
|
||||
manifest.latest_package_identifier = "langgenius/openai:1.0.0@abc"
|
||||
mocker.patch(
|
||||
"services.plugin.plugin_migration.marketplace.batch_fetch_plugin_manifests",
|
||||
return_value=[manifest],
|
||||
)
|
||||
|
||||
result = PluginMigration._fetch_plugin_unique_identifier("langgenius/openai")
|
||||
|
||||
assert result == "langgenius/openai:1.0.0@abc"
|
||||
|
||||
|
||||
class TestHandlePluginInstanceInstall:
|
||||
def test_raises_when_disabled_and_map_nonempty(self) -> None:
|
||||
with patch(f"{MIGRATION_MODULE}.dify_config") as mock_cfg:
|
||||
mock_cfg.MARKETPLACE_ENABLED = False
|
||||
|
||||
with pytest.raises(ValueError, match="Marketplace disabled"):
|
||||
PluginMigration.handle_plugin_instance_install(
|
||||
"tenant1", {"langgenius/openai": "langgenius/openai:1.0.0@abc"}
|
||||
)
|
||||
|
||||
def test_no_raise_when_disabled_and_map_empty(self) -> None:
|
||||
with (
|
||||
patch(f"{MIGRATION_MODULE}.dify_config") as mock_cfg,
|
||||
patch(f"{MIGRATION_MODULE}.PluginInstaller") as mock_installer_cls,
|
||||
):
|
||||
mock_cfg.MARKETPLACE_ENABLED = False
|
||||
mock_installer = MagicMock()
|
||||
mock_installer_cls.return_value = mock_installer
|
||||
mock_installer.install_from_identifiers.return_value = MagicMock(all_installed=True)
|
||||
|
||||
result = PluginMigration.handle_plugin_instance_install("tenant1", {})
|
||||
|
||||
assert isinstance(result, dict)
|
||||
|
||||
def test_proceeds_when_enabled(self) -> None:
|
||||
with (
|
||||
patch(f"{MIGRATION_MODULE}.dify_config") as mock_cfg,
|
||||
patch(f"{MIGRATION_MODULE}.marketplace") as mock_marketplace,
|
||||
patch(f"{MIGRATION_MODULE}.PluginInstaller") as mock_installer_cls,
|
||||
):
|
||||
mock_cfg.MARKETPLACE_ENABLED = True
|
||||
mock_marketplace.download_plugin_pkg.return_value = b"pkg_data"
|
||||
mock_installer = MagicMock()
|
||||
mock_installer_cls.return_value = mock_installer
|
||||
mock_installer.install_from_identifiers.return_value = MagicMock(all_installed=True)
|
||||
|
||||
result = PluginMigration.handle_plugin_instance_install(
|
||||
"tenant1", {"langgenius/openai": "langgenius/openai:1.0.0@abc"}
|
||||
)
|
||||
|
||||
mock_marketplace.download_plugin_pkg.assert_called_once()
|
||||
assert "success" in result or "failed" in result
|
||||
50
api/tests/unit_tests/services/plugin/test_plugin_service.py
Normal file
@ -0,0 +1,50 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
MODULE = "services.plugin.plugin_service"
|
||||
|
||||
|
||||
class TestFetchLatestPluginVersion:
|
||||
def test_skips_marketplace_fetch_when_disabled(self) -> None:
|
||||
"""Cache misses stay None; marketplace is never called when disabled."""
|
||||
with (
|
||||
patch(f"{MODULE}.dify_config") as mock_cfg,
|
||||
patch(f"{MODULE}.redis_client") as mock_redis,
|
||||
patch(f"{MODULE}.marketplace") as mock_marketplace,
|
||||
):
|
||||
mock_cfg.MARKETPLACE_ENABLED = False
|
||||
mock_redis.get.return_value = None # all cache misses
|
||||
|
||||
from services.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_latest_plugin_version(["langgenius/openai", "langgenius/anthropic"])
|
||||
|
||||
mock_marketplace.batch_fetch_plugin_manifests.assert_not_called()
|
||||
assert result == {"langgenius/openai": None, "langgenius/anthropic": None}
|
||||
|
||||
def test_calls_marketplace_fetch_when_enabled(self) -> None:
|
||||
"""Cache misses trigger marketplace fetch when enabled."""
|
||||
manifest = MagicMock()
|
||||
manifest.plugin_id = "langgenius/openai"
|
||||
manifest.latest_version = "1.0.0"
|
||||
manifest.latest_package_identifier = "langgenius/openai:1.0.0@abc"
|
||||
manifest.status = "active"
|
||||
manifest.deprecated_reason = ""
|
||||
manifest.alternative_plugin_id = ""
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}.dify_config") as mock_cfg,
|
||||
patch(f"{MODULE}.redis_client") as mock_redis,
|
||||
patch(f"{MODULE}.marketplace") as mock_marketplace,
|
||||
):
|
||||
mock_cfg.MARKETPLACE_ENABLED = True
|
||||
mock_redis.get.return_value = None
|
||||
mock_marketplace.batch_fetch_plugin_manifests.return_value = [manifest]
|
||||
|
||||
from services.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_latest_plugin_version(["langgenius/openai"])
|
||||
|
||||
# The list arg is mutated by remove() after the call, so check call count + result.
|
||||
mock_marketplace.batch_fetch_plugin_manifests.assert_called_once()
|
||||
assert result["langgenius/openai"] is not None
|
||||
assert result["langgenius/openai"].version == "1.0.0"
|
||||
@ -0,0 +1,36 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from services.rag_pipeline.rag_pipeline import RagPipelineService
|
||||
|
||||
|
||||
def _make_service() -> RagPipelineService:
|
||||
return RagPipelineService.__new__(RagPipelineService)
|
||||
|
||||
|
||||
def test_fetch_recommended_plugin_manifests_returns_empty_when_disabled(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.dify_config.MARKETPLACE_ENABLED", False)
|
||||
batch_fetch = mocker.patch("services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids")
|
||||
|
||||
service = _make_service()
|
||||
result = service._fetch_recommended_plugin_manifests(["langgenius/openai"])
|
||||
|
||||
assert result == []
|
||||
batch_fetch.assert_not_called()
|
||||
|
||||
|
||||
def test_fetch_recommended_plugin_manifests_returns_data_when_enabled(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline.dify_config.MARKETPLACE_ENABLED", True)
|
||||
expected = [{"plugin_id": "langgenius/openai", "name": "OpenAI"}]
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids",
|
||||
return_value=expected,
|
||||
)
|
||||
|
||||
service = _make_service()
|
||||
result = service._fetch_recommended_plugin_manifests(["langgenius/openai"])
|
||||
|
||||
assert result == expected
|
||||
@ -1,8 +1,10 @@
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from models.dataset import Dataset
|
||||
from services.entities.knowledge_entities.rag_pipeline_entities import KnowledgeConfiguration
|
||||
@ -514,3 +516,64 @@ def test_deal_document_data_upload_file_with_existing_file(mocker) -> None:
|
||||
assert document.data_source_type == "local_file"
|
||||
assert "real_file_id" in document.data_source_info
|
||||
assert add_mock.call_count >= 2
|
||||
|
||||
|
||||
def _make_service():
|
||||
return RagPipelineTransformService.__new__(RagPipelineTransformService)
|
||||
|
||||
|
||||
def test_deal_dependencies_skips_marketplace_when_disabled(mocker: MockerFixture, caplog) -> None:
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline_transform_service.dify_config.MARKETPLACE_ENABLED",
|
||||
False,
|
||||
)
|
||||
installer = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginInstaller").return_value
|
||||
installer.list_plugins.return_value = []
|
||||
mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginMigration")
|
||||
install_call = mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline_transform_service.PluginService.install_from_marketplace_pkg"
|
||||
)
|
||||
|
||||
pipeline_yaml = {
|
||||
"dependencies": [
|
||||
{
|
||||
"type": "marketplace",
|
||||
"value": {"plugin_unique_identifier": "langgenius/openai:1.0.0@abc"},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
service = _make_service()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
service._deal_dependencies(pipeline_yaml, "tenant-1")
|
||||
|
||||
install_call.assert_not_called()
|
||||
assert any("Marketplace disabled" in rec.message for rec in caplog.records)
|
||||
|
||||
|
||||
def test_deal_dependencies_installs_when_enabled(mocker: MockerFixture) -> None:
|
||||
mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline_transform_service.dify_config.MARKETPLACE_ENABLED",
|
||||
True,
|
||||
)
|
||||
installer = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginInstaller").return_value
|
||||
installer.list_plugins.return_value = []
|
||||
migration = mocker.patch("services.rag_pipeline.rag_pipeline_transform_service.PluginMigration").return_value
|
||||
migration._fetch_plugin_unique_identifier.return_value = "langgenius/openai:1.0.0@abc"
|
||||
install_call = mocker.patch(
|
||||
"services.rag_pipeline.rag_pipeline_transform_service.PluginService.install_from_marketplace_pkg"
|
||||
)
|
||||
|
||||
pipeline_yaml = {
|
||||
"dependencies": [
|
||||
{
|
||||
"type": "marketplace",
|
||||
"value": {"plugin_unique_identifier": "langgenius/openai:1.0.0@abc"},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
service = _make_service()
|
||||
service._deal_dependencies(pipeline_yaml, "tenant-1")
|
||||
|
||||
install_call.assert_called_once_with("tenant-1", ["langgenius/openai:1.0.0@abc"])
|
||||
|
||||
@ -5,7 +5,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
||||
### What's Updated
|
||||
|
||||
- **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections.\
|
||||
For more information, refer `docker/certbot/README.md`.
|
||||
For more information, refer to `docker/certbot/README.md`.
|
||||
|
||||
- **Persistent Environment Variables**: Essential startup defaults are provided in `.env.example`, while local values are stored in `.env`, ensuring that your configurations persist across deployments.
|
||||
|
||||
@ -17,26 +17,26 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
||||
### How to Deploy Dify with `docker-compose.yaml`
|
||||
|
||||
1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system.
|
||||
1. **Environment Setup**:
|
||||
2. **Environment Setup**:
|
||||
- Navigate to the `docker` directory.
|
||||
- Copy `.env.example` to `.env`.
|
||||
- Customize `.env` when you need to change essential startup defaults. Copy optional files from `envs/` without the `.example` suffix when you need advanced settings.
|
||||
- **Optional (for advanced deployments)**:
|
||||
If you maintain a full `.env` file copied from `.env.example`, you may use the environment synchronization tool to keep it aligned with the latest `.env.example` updates while preserving your custom settings.
|
||||
See the [Environment Variables Synchronization](#environment-variables-synchronization) section below.
|
||||
1. **Running the Services**:
|
||||
3. **Running the Services**:
|
||||
- Execute `docker compose up -d` from the `docker` directory to start the services.
|
||||
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
|
||||
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`. See `envs/vectorstores/` for the full list of supported options.
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
1. **SSL Certificate Setup**:
|
||||
- Refer `docker/certbot/README.md` to set up SSL certificates using Certbot.
|
||||
1. **OpenTelemetry Collector Setup**:
|
||||
- Change `ENABLE_OTEL` to `true` in `.env`.
|
||||
- Configure `OTLP_BASE_ENDPOINT` properly.
|
||||
4. **SSL Certificate Setup**:
|
||||
- Refer to `docker/certbot/README.md` to set up SSL certificates using Certbot.
|
||||
5. **OpenTelemetry Collector Setup**:
|
||||
- Copy `envs/core-services/shared.env.example` to `envs/core-services/shared.env`.
|
||||
- Set `ENABLE_OTEL=true` and configure `OTLP_BASE_ENDPOINT`. Tune the other `OTEL_*` knobs in the same file if needed.
|
||||
|
||||
### How to Deploy Middleware for Developing Dify
|
||||
|
||||
@ -44,7 +44,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
||||
- Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches.
|
||||
- Navigate to the `docker` directory.
|
||||
- Ensure the `middleware.env` file is created by running `cp envs/middleware.env.example middleware.env` (refer to the `envs/middleware.env.example` file).
|
||||
1. **Running Middleware Services**:
|
||||
2. **Running Middleware Services**:
|
||||
- Navigate to the `docker` directory.
|
||||
- Execute `docker compose --env-file middleware.env -f docker-compose.middleware.yaml -p dify up -d` to start PostgreSQL/MySQL (per `DB_TYPE`) plus the bundled Weaviate instance.
|
||||
|
||||
@ -55,9 +55,9 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
||||
For users migrating from the `docker-legacy` setup:
|
||||
|
||||
1. **Review Changes**: Familiarize yourself with the new `.env` configuration and Docker Compose setup.
|
||||
1. **Transfer Customizations**:
|
||||
2. **Transfer Customizations**:
|
||||
- If you have customized configurations such as `docker-compose.yaml`, `ssrf_proxy/squid.conf`, or `nginx/conf.d/default.conf`, you will need to reflect these changes in the `.env` file you create.
|
||||
1. **Data Migration**:
|
||||
3. **Data Migration**:
|
||||
- Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary.
|
||||
|
||||
### Overview of `.env`, `.env.example`, and `envs/`
|
||||
@ -80,49 +80,51 @@ The root `.env.example` file contains the essential startup settings. Optional a
|
||||
|
||||
1. **Common Variables**:
|
||||
|
||||
- `CONSOLE_API_URL`, `SERVICE_API_URL`: URLs for different API services.
|
||||
- `APP_WEB_URL`: Frontend application URL.
|
||||
- `FILES_URL`: Base URL for file downloads and previews.
|
||||
- `CONSOLE_API_URL`, `CONSOLE_WEB_URL`, `SERVICE_API_URL`, `APP_API_URL`, `APP_WEB_URL`: URLs for the API and frontend services.
|
||||
- `FILES_URL`, `INTERNAL_FILES_URL`: Public and internal base URLs for file downloads and previews.
|
||||
- `ENDPOINT_URL_TEMPLATE`, `NEXT_PUBLIC_SOCKET_URL`, `TRIGGER_URL`: Additional service URLs.
|
||||
|
||||
See `.env.example` for the full list.
|
||||
|
||||
1. **Server Configuration**:
|
||||
2. **Server Configuration**:
|
||||
|
||||
- `LOG_LEVEL`, `DEBUG`, `FLASK_DEBUG`: Logging and debug settings.
|
||||
- `SECRET_KEY`: A key for signing sessions, JWTs, and file URLs. Leave it empty to let Dify generate a persistent key in the storage directory, or set a unique value yourself.
|
||||
|
||||
1. **Database Configuration**:
|
||||
3. **Database Configuration**:
|
||||
|
||||
- `DB_USERNAME`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`: PostgreSQL database credentials and connection details.
|
||||
|
||||
1. **Redis Configuration**:
|
||||
4. **Redis Configuration**:
|
||||
|
||||
- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis server connection settings.
|
||||
- `REDIS_KEY_PREFIX`: Optional global namespace prefix for Redis keys, topics, streams, and Celery Redis transport artifacts.
|
||||
|
||||
1. **Celery Configuration**:
|
||||
5. **Celery Configuration**:
|
||||
|
||||
- `CELERY_BROKER_URL`: Configuration for Celery message broker.
|
||||
|
||||
1. **Storage Configuration**:
|
||||
6. **Storage Configuration**:
|
||||
|
||||
- `STORAGE_TYPE`, `OPENDAL_SCHEME`, `OPENDAL_FS_ROOT`: Default local file storage settings. Optional storage backends are configured from the files under `envs/`.
|
||||
|
||||
1. **Vector Database Configuration**:
|
||||
7. **Vector Database Configuration**:
|
||||
|
||||
- `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`).
|
||||
- `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`). See `envs/vectorstores/` for the full list of supported options.
|
||||
- Specific settings for each vector store like `WEAVIATE_ENDPOINT`, `MILVUS_URI`.
|
||||
|
||||
1. **CORS Configuration**:
|
||||
8. **CORS Configuration**:
|
||||
|
||||
- `WEB_API_CORS_ALLOW_ORIGINS`, `CONSOLE_CORS_ALLOW_ORIGINS`: Settings for cross-origin resource sharing.
|
||||
|
||||
1. **OpenTelemetry Configuration**:
|
||||
9. **OpenTelemetry Configuration**:
|
||||
|
||||
- `ENABLE_OTEL`: Enable OpenTelemetry collector in api.
|
||||
- `OTLP_BASE_ENDPOINT`: Endpoint for your OTLP exporter.
|
||||
|
||||
1. **Other Service-Specific Environment Variables**:
|
||||
10. **Other Service-Specific Environment Variables**:
|
||||
|
||||
- Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`.
|
||||
- Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`.
|
||||
|
||||
### Environment Variables Synchronization
|
||||
|
||||
|
||||
140
docs/integrations-route-contract.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Integrations Route Contract
|
||||
|
||||
This document records the current canonical routes for the Integrations navigation, the legacy routes that redirect to them, and the remaining migration gaps. The first migration only moves existing pages onto the new routes; UI redesign work is out of scope.
|
||||
|
||||
## Current Status
|
||||
|
||||
Completed:
|
||||
|
||||
| Area | Status |
|
||||
| --- | --- |
|
||||
| Canonical `/integrations/...` route adapter | Implemented in `web/app/(commonLayout)/integrations/[[...slug]]/page.tsx`. |
|
||||
| Route contract utility | Implemented in `web/app/components/tools/integration-routes.ts`. |
|
||||
| Existing page reuse | Implemented through `IntegrationSectionRenderer`; no duplicated UI copy. |
|
||||
| Legacy `/tools?...` redirects | Implemented through `web/app/(commonLayout)/tools/page.tsx`. |
|
||||
| Legacy `/plugins` installed redirects | Implemented through `web/app/(commonLayout)/plugins/page.tsx`. |
|
||||
| Tools tab navigation under new URLs | Implemented; scoped tool tabs push canonical `/integrations/tools/...` URLs. |
|
||||
| Singular-only canonical URLs | Implemented; plural and misplaced aliases are intentionally unsupported. |
|
||||
|
||||
Not completed:
|
||||
|
||||
| Area | Remaining work |
|
||||
| --- | --- |
|
||||
| Integrations overview page | Not introduced; `/integrations` currently redirects to `/integrations/model-provider`. |
|
||||
| Tools overview page | Not introduced; `/integrations/tools` currently redirects to `/integrations/tools/built-in`. |
|
||||
| Plugin route migration | `/plugins` still owns the old plugin management and marketplace surface; no `/integrations/plugin` route will be introduced. Non-marketplace plugin URLs should redirect to `/integrations`. |
|
||||
| Marketplace route migration | `/marketplace/...` routes below are future recommendations only; they are not implemented here. |
|
||||
| New onboarding UI redesign | Not started in this route migration; current pages intentionally reuse existing UI. |
|
||||
| Marketplace plugin redirects | Not implemented; marketplace plugin URLs intentionally keep rendering the legacy plugin marketplace surface for now. |
|
||||
|
||||
## Navigation Labels
|
||||
|
||||
| Navigation item | Canonical label |
|
||||
| --- | --- |
|
||||
| Model Provider | Model Provider |
|
||||
| Built-in tools | Built-in |
|
||||
| Custom Tool | Swagger API as Tool |
|
||||
| Workflow | Workflow as Tool |
|
||||
| MCP | MCP |
|
||||
| Data Source | Data Source |
|
||||
| API Extension | API Extension |
|
||||
| Plugins | Plugins |
|
||||
| Marketplace | Marketplace |
|
||||
|
||||
## Canonical Integrations Routes
|
||||
|
||||
| Route | Destination |
|
||||
| --- | --- |
|
||||
| `/integrations` | Redirect to `/integrations/model-provider` unless an overview page is introduced. |
|
||||
| `/integrations/model-provider` | Existing model provider management page. |
|
||||
| `/integrations/tools` | Redirect to `/integrations/tools/built-in` unless a tools overview page is introduced. |
|
||||
| `/integrations/tools/built-in` | Existing built-in tools list. |
|
||||
| `/integrations/tool/api` | Existing custom API tool list, relabeled as Swagger API as Tool. |
|
||||
| `/integrations/tools/workflow` | Existing Workflow as Tool management page. |
|
||||
| `/integrations/tools/mcp` | Existing MCP tools management page. |
|
||||
| `/integrations/trigger` | Existing plugin trigger list filtered from plugin management. |
|
||||
| `/integrations/agent-strategy` | Existing agent strategy plugin list filtered from plugin management. |
|
||||
| `/integrations/extension` | Existing extension plugin list filtered from plugin management. |
|
||||
| `/integrations/data-source` | Existing data source page. |
|
||||
| `/integrations/tools/api-extension` | Existing API extension page under the Tools group. |
|
||||
|
||||
## Integration Plugin Category Routes
|
||||
|
||||
These navigation items use plugin categories from the existing plugin management surface:
|
||||
|
||||
| Navigation item | Plugin category | Route |
|
||||
| --- | --- | --- |
|
||||
| Trigger | `trigger` | `/integrations/trigger` |
|
||||
| Agent Strategy | `agent-strategy` | `/integrations/agent-strategy` |
|
||||
| Extension | `extension` | `/integrations/extension` |
|
||||
|
||||
The install and filter controls in the Integrations sidebar are disabled actions, not route destinations.
|
||||
|
||||
These routes reuse the installed plugin management list with an initial category filter. They are not marketplace category pages.
|
||||
|
||||
Do not treat every plugin category as an Integrations navigation item automatically. `trigger`, `agent-strategy`, and `extension` are currently exposed under Integrations because they are explicit navigation items. Other plugin categories have different product meanings:
|
||||
|
||||
| Plugin category | Integrations relationship |
|
||||
| --- | --- |
|
||||
| `tool` | Not equal to `/integrations/tools/...`; tool plugins can expose tool providers that appear in Tools, but the Tools page is provider-based. |
|
||||
| `model` | Not equal to `/integrations/model-provider`; model providers are managed through the model provider page. |
|
||||
| `datasource` | Not equal to the full Data Source page; data source integrations have their own existing page. |
|
||||
| `trigger` | Reused as `/integrations/trigger`, installed plugins filtered by category. |
|
||||
| `agent-strategy` | Reused as `/integrations/agent-strategy`, installed plugins filtered by category. |
|
||||
| `extension` | Reused as `/integrations/extension`, installed plugins filtered by category. |
|
||||
|
||||
## Legacy Tools Redirects
|
||||
|
||||
| Legacy route | New route |
|
||||
| --- | --- |
|
||||
| `/tools` | `/integrations/tools/built-in` |
|
||||
| `/tools?section=provider` | `/integrations/model-provider` |
|
||||
| `/tools?section=builtin` | `/integrations/tools/built-in` |
|
||||
| `/tools?section=builtin&category=builtin` | `/integrations/tools/built-in` |
|
||||
| `/tools?category=builtin` | `/integrations/tools/built-in` |
|
||||
| `/tools?section=custom-tool` | `/integrations/tool/api` |
|
||||
| `/tools?section=custom-tool&category=api` | `/integrations/tool/api` |
|
||||
| `/tools?category=api` | `/integrations/tool/api` |
|
||||
| `/tools?section=workflow-tool` | `/integrations/tools/workflow` |
|
||||
| `/tools?section=workflow-tool&category=workflow` | `/integrations/tools/workflow` |
|
||||
| `/tools?category=workflow` | `/integrations/tools/workflow` |
|
||||
| `/tools?section=mcp` | `/integrations/tools/mcp` |
|
||||
| `/tools?section=mcp&category=mcp` | `/integrations/tools/mcp` |
|
||||
| `/tools?category=mcp` | `/integrations/tools/mcp` |
|
||||
| `/tools?section=data-source` | `/integrations/data-source` |
|
||||
| `/tools?section=api-based-extension` | `/integrations/tools/api-extension` |
|
||||
| `/tools?section=trigger` | `/integrations/trigger` |
|
||||
| `/tools?section=agent-strategy` | `/integrations/agent-strategy` |
|
||||
| `/tools?section=extension` | `/integrations/extension` |
|
||||
|
||||
Preserve non-routing query parameters such as `q`, `tags`, and `sort`, but drop legacy routing parameters such as `section` and `category` during redirects.
|
||||
|
||||
## Non-Canonical Integrations Routes
|
||||
|
||||
Do not add plural or misplaced alias redirects for new Integrations URLs. Only the singular canonical routes above should resolve. For example, `/integrations/model-providers`, `/integrations/data-sources`, `/integrations/api-extensions`, `/integrations/tools/trigger`, `/integrations/tools/agent-strategy`, and `/integrations/tools/extension` should not be treated as supported URLs unless they are later confirmed to have shipped externally.
|
||||
|
||||
## Legacy Plugin Redirects
|
||||
|
||||
Plugins have two different product meanings today: installed plugin management and marketplace discovery. Only the non-marketplace plugin URLs should redirect into Integrations. There is no `/integrations/plugin` route.
|
||||
|
||||
| Old Plugin URL | Recommended redirect | Reason |
|
||||
| --- | --- | --- |
|
||||
| `/plugins` | `/integrations` | Installed plugin management entry should move into the Integrations main entry. |
|
||||
| `/plugins?tab=plugins` | `/integrations` | Explicit installed plugins tab; non-marketplace semantics. |
|
||||
| `/plugins?tab=discover` | Do not redirect to Integrations | Marketplace discovery. |
|
||||
| `/plugins?tab=all` | Do not redirect to Integrations | Marketplace category: all. |
|
||||
| `/plugins?tab=tool` | Do not redirect to Integrations | Marketplace tool category, not installed tools management. |
|
||||
| `/plugins?tab=model` | Do not redirect to Integrations | Marketplace model category. |
|
||||
| `/plugins?tab=trigger` | Do not redirect to Integrations | Marketplace trigger category. |
|
||||
| `/plugins?tab=agent-strategy` | Do not redirect to Integrations | Marketplace agent strategy category. |
|
||||
| `/plugins?tab=extension` | Do not redirect to Integrations | Marketplace extension category. |
|
||||
| `/plugins?tab=datasource` | Do not redirect to Integrations | Marketplace datasource category. |
|
||||
| `/plugins?tab=bundle` | Do not redirect to Integrations | Marketplace bundle category. |
|
||||
|
||||
## Migration Order
|
||||
|
||||
1. Add the canonical route map and route tests.
|
||||
2. Mount the existing pages under the new Integrations routes without UI redesign.
|
||||
3. Update internal links to generate canonical URLs.
|
||||
4. Add legacy redirects for `/tools` and `/plugins`.
|
||||
5. Keep compatibility tests for each legacy route until old links can be removed.
|
||||
189
docs/main-nav-gating-follow-ups.md
Normal file
@ -0,0 +1,189 @@
|
||||
# Main Nav Gating Follow-ups
|
||||
|
||||
Context: the desktop MainNav rewrite moved several workspace, account, tools, and marketplace entry points out of the old header/account-setting layout. These notes track product-contract questions that should be resolved before treating the rewrite as behavior-complete.
|
||||
|
||||
Current status:
|
||||
|
||||
- Open: account-setting modal navigation API naming, Marketplace/Integrations install task status parity, account language/timezone access parity.
|
||||
- Partially resolved: Apps/Datasets quick-switch/create parity.
|
||||
- Resolved: Integrations sidebar placeholder state, branding-gated Help trigger, workspace plan billing access, Integrations plugin install permission gating.
|
||||
|
||||
## 1. Account-setting modal naming and moved destinations
|
||||
|
||||
Status: Open.
|
||||
|
||||
Current branch behavior:
|
||||
|
||||
- `setShowAccountSettingModal(PROVIDER)` routes to `/integrations/model-provider`.
|
||||
- `setShowAccountSettingModal(DATA_SOURCE)` routes to `/integrations/data-source`.
|
||||
- `setShowAccountSettingModal(API_BASED_EXTENSION)` routes to `/integrations/tools/api-extension`.
|
||||
- Document Settings no longer directly renders the old Account Settings modal for Provider; it uses the Integrations destination helper.
|
||||
|
||||
Old behavior: these calls opened the account-setting modal and switched to the matching tab.
|
||||
|
||||
Question:
|
||||
|
||||
- Since Provider, Data Source, and API-based Extension are no longer inside Account Settings in the new design, should this API still be named `setShowAccountSettingModal` for those destinations?
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Either keep the compatibility shim but document that these payloads are now route destinations, or introduce a clearer navigation API for integration destinations and update call sites intentionally.
|
||||
- Re-check call sites launched from workflows, datasets, and app configuration when new entry points are added. The known Document Settings provider entry has been migrated.
|
||||
|
||||
## 2. Integrations sidebar disabled entries
|
||||
|
||||
Status: Resolved.
|
||||
|
||||
Previous branch behavior:
|
||||
|
||||
- Integrations includes disabled entries for Trigger, Agent Strategy, and Extension.
|
||||
- These are visible but not actionable.
|
||||
|
||||
Current branch behavior:
|
||||
|
||||
- Trigger, Agent Strategy, and Extension are no longer disabled placeholders.
|
||||
- These sections route to `PluginCategoryPage` with the corresponding plugin category.
|
||||
|
||||
Resolution:
|
||||
|
||||
- No remaining disabled-entry gating decision is tracked here.
|
||||
|
||||
## 3. Plugin and marketplace status parity
|
||||
|
||||
Status: Open.
|
||||
|
||||
Old header behavior:
|
||||
|
||||
- `PluginsNav` showed plugin install progress and error state through the installing icon and red indicator.
|
||||
- Installing tasks showed the downloading icon.
|
||||
- Failed or erroring install tasks showed the red status indicator.
|
||||
|
||||
Current MainNav behavior:
|
||||
|
||||
- MainNav has a Marketplace link, but it does not surface plugin installing/error state.
|
||||
- Integrations has install entry points, but it does not surface the old `PluginTasks` install-task status entry near the Integrations install action.
|
||||
- Marketplace is the product discovery surface for uninstalled integrations; `PluginTasks` is only the transient install-task status inbox for running/succeeded/failed installs.
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Decide whether MainNav Marketplace should preserve the old plugin task status indicator.
|
||||
- Decide whether Integrations should expose `PluginTasks` near the install action so users can inspect failed/running install tasks without returning to the old `/plugins` shell.
|
||||
- If yes, reuse the existing `usePluginTaskStatus` behavior instead of creating a parallel status source.
|
||||
|
||||
## 4. Account language and timezone access
|
||||
|
||||
Status: Open.
|
||||
|
||||
Current branch behavior:
|
||||
|
||||
- Desktop MainNav account menu includes Language and Timezone submenus.
|
||||
- The main app layout now uses MainNav across breakpoints.
|
||||
- The default account dropdown does not expose a direct Language/Timezone settings entry.
|
||||
- The default account dropdown still exists in non-MainNav account/header surfaces such as the account layout.
|
||||
- Language and Timezone still belong to Account Settings, not Integrations.
|
||||
- `UpdateSettingPopover` still links the timezone hint to `ACCOUNT_SETTING_TAB.LANGUAGE`.
|
||||
- The legacy `ReferenceSettingModal` auto-update timezone hint also still links to `ACCOUNT_SETTING_TAB.LANGUAGE`.
|
||||
|
||||
Decision:
|
||||
|
||||
- Preserve the old language-access contract across breakpoints.
|
||||
- The desktop MainNav path is acceptable; the remaining question is default account-dropdown parity wherever that path remains active, plus whether hidden Account Settings Language entry points should remain acceptable.
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Add an equivalent language/timezone entry to the default account path, or otherwise ensure users in those surfaces can still reach language settings.
|
||||
- Decide whether the Update Setting timezone hint should keep opening the hidden Account Settings Language page, or whether Account Settings should surface Language in its visible menu.
|
||||
- Keep this as gate-contract parity, not a visual requirement to recreate the old Account Settings sidebar.
|
||||
|
||||
## 5. Apps and Datasets quick-switch/create parity
|
||||
|
||||
Status: Partially resolved.
|
||||
|
||||
Old header behavior:
|
||||
|
||||
- `AppNav` could show the current app, list more apps, load more results, and launch create-app flows from the header nav.
|
||||
- `DatasetNav` could show the current dataset, list more datasets, load more results, and launch dataset creation from the header nav.
|
||||
|
||||
Current MainNav behavior:
|
||||
|
||||
- Apps is no longer only a static navigation link: MainNav includes a Web Apps section with installed web app search, pin, delete, and navigation behavior.
|
||||
- Apps still does not preserve the old `AppNav` current-app switcher, load-more behavior, or create-app flows.
|
||||
- Datasets is still a navigation link and does not preserve the old `DatasetNav` current-dataset switcher, load-more behavior, or dataset creation entry.
|
||||
|
||||
Follow-up decision needed:
|
||||
|
||||
- Decide whether the new design intentionally removes these quick-switch/create affordances.
|
||||
- If not, add equivalent behavior in the MainNav flow without copying the old header UI directly.
|
||||
|
||||
## 6. Branding-gated Help and Support behavior
|
||||
|
||||
Status: Resolved.
|
||||
|
||||
Old account dropdown behavior:
|
||||
|
||||
- When `systemFeatures.branding.enabled` is `true`, the whole Dify help/community group is hidden.
|
||||
- That hidden group includes Docs, Support, Compliance, Roadmap, GitHub, and About.
|
||||
|
||||
Current MainNav behavior:
|
||||
|
||||
- `HelpMenu` returns `null` when `systemFeatures.branding.enabled` is `true`.
|
||||
- This prevents the empty Help trigger/popup path.
|
||||
|
||||
Resolution:
|
||||
|
||||
- Current implementation follows strict old parity for MainNav: the whole Help trigger is hidden for branded deployments.
|
||||
|
||||
Optional future product question:
|
||||
|
||||
- If branded deployments should retain configured customer support channels, split Support into customer-support and Dify-community items with separate gates.
|
||||
|
||||
## 7. Paid plan Billing access from workspace plan
|
||||
|
||||
Status: Resolved.
|
||||
|
||||
Old header behavior:
|
||||
|
||||
- The header plan badge was clickable.
|
||||
- For sandbox/free plans, clicking the badge opened the pricing modal.
|
||||
- For non-sandbox paid plans, clicking the badge opened Account Settings on the Billing tab.
|
||||
|
||||
Current MainNav behavior:
|
||||
|
||||
- Sandbox/free plans have an explicit Upgrade action in the WorkspaceCard credit row.
|
||||
- Non-sandbox paid plans have an explicit View Plan action in the same plan-action row.
|
||||
- Both actions open the pricing modal.
|
||||
- The workspace plan badge is display-only.
|
||||
- The WorkspaceCard Settings menu item routes to Account Settings on the Billing tab.
|
||||
- Invite Members remains the Members entry, so Settings and Invite Members do not duplicate the same destination.
|
||||
|
||||
Resolution:
|
||||
|
||||
- Keep sandbox/free and paid behavior as the explicit plan-action row.
|
||||
- Keep the workspace plan badge display-only.
|
||||
- Use the WorkspaceCard Settings item as the Billing entry.
|
||||
|
||||
## 8. Integrations plugin install permission gating
|
||||
|
||||
Status: Resolved.
|
||||
|
||||
Old `/plugins` behavior:
|
||||
|
||||
- `InstallPluginDropdown` is shown only when `canManagement` is true.
|
||||
- The plugins page drag-and-drop install uploader is enabled only when the plugins tab is active and `canManagement` is true.
|
||||
|
||||
Previous Integrations behavior:
|
||||
|
||||
- The Integrations sidebar install dropdown remains visible.
|
||||
- Trigger and Agent Strategy empty states show Marketplace, GitHub, and Local Package File install entry points according to marketplace/local-package feature gates.
|
||||
- Trigger and Agent Strategy drag-and-drop package install is gated by `restrict_to_marketplace_only`, not by `canManagement`.
|
||||
|
||||
Current Integrations behavior:
|
||||
|
||||
- The Integrations sidebar install dropdown is shown only when `canManagement` is true.
|
||||
- Trigger, Agent Strategy, and Extension empty-state install methods are hidden when `canInstall` is false.
|
||||
- Trigger, Agent Strategy, and Extension drag-and-drop package install is gated by both `canInstall` and `restrict_to_marketplace_only`.
|
||||
- Installed-package success can redirect to the actual installed integration category when the install context differs.
|
||||
|
||||
Resolution:
|
||||
|
||||
- Current implementation follows strict old `/plugins` install permission behavior for installation entry points while keeping Marketplace as a separate navigation surface.
|
||||
@ -265,6 +265,12 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@utility title-5xl-semi-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@utility title-5xl-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
|
||||
43
packages/iconify-collections/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# @dify/iconify-collections
|
||||
|
||||
Pre-generated Iconify collections for Dify custom SVG icons. The web app imports these collections from this package so Tailwind does not need to scan and build custom SVG icon data from the old `web/app/components/base/icons/src` tree during dev startup.
|
||||
|
||||
## Adding Custom SVG Icons
|
||||
|
||||
Add new SVG source files under one of these directories:
|
||||
|
||||
- `assets/public/...` for multi-color or public brand-like icons.
|
||||
- `assets/vender/...` for UI vendor icons that should render with `currentColor`.
|
||||
|
||||
After adding or changing SVG files, regenerate the packaged collections:
|
||||
|
||||
```bash
|
||||
pnpm --filter @dify/iconify-collections generate
|
||||
```
|
||||
|
||||
Then run the dimension guard:
|
||||
|
||||
```bash
|
||||
pnpm --filter @dify/iconify-collections check:dimensions
|
||||
```
|
||||
|
||||
This protects existing icon groups with layout-sensitive intrinsic sizes, such as the `main-nav-*` icons that must remain `20x20` after collection flattening.
|
||||
|
||||
Commit both the SVG source files and the generated package files under `custom-public/` or `custom-vender/`.
|
||||
Restart the web dev server after regenerating icons. Tailwind loads this plugin collection at startup, so an already-running dev server may not render newly-added `i-custom-*` classes until it restarts.
|
||||
|
||||
Use the generated icons through Tailwind icon classes in frontend code. For example:
|
||||
|
||||
```text
|
||||
assets/vender/integrations/mcp.svg
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```tsx
|
||||
<span aria-hidden className="i-custom-vender-integrations-mcp size-4" />
|
||||
```
|
||||
|
||||
Do not add new generated React icon components or JSON files under `web/app/components/base/icons/src/...` for new custom SVG icons. That path is legacy; new custom icons should flow through this package and be consumed as `i-custom-*` classes.
|
||||
|
||||
When reviewing generated `icons.json` diffs, check that unrelated existing icon groups did not lose or change their intrinsic `width` and `height`. If a group is layout-sensitive, add it to `scripts/check-icon-dimensions.ts`.
|
||||
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM13.4357 12.0683L12.3262 11.3286C12.9624 10.3761 13.3333 9.2314 13.3333 8.00007C13.3333 7.65933 13.3049 7.3252 13.2504 7L14.5457 6.66667C14.6252 7.09907 14.6667 7.54467 14.6667 8.00007C14.6667 9.50507 14.2133 10.9041 13.4357 12.0683Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM5.33333 2.66667C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 7.58993 1.64545 8.46193 2.21052 9.1666C2.93977 10.076 3.33357 11.1115 3.3335 12.2042L3.33342 13.3333H6.66713L6.6678 11.3333H9.33333V8.50127L10.3665 8.05873L9.33813 6.44175L9.30007 6.14745C9.04433 4.16761 7.34953 2.66667 5.33333 2.66667ZM12.3262 11.3286L13.4357 12.0683C14.2133 10.9041 14.6667 9.50507 14.6667 8.00007C14.6667 7.54467 14.6252 7.09907 14.5457 6.66667L13.2504 7C13.3049 7.3252 13.3333 7.65933 13.3333 8.00007C13.3333 9.2314 12.9624 10.3761 12.3262 11.3286Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,10 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<g id="Vector">
|
||||
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095ZM13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095ZM9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324ZM11.1296 4.324C11.1296 5.82362 10.0748 7.07639 8.66667 7.38194V7.9197L9.74284 8.71398C10.3012 8.19618 11.0489 7.87934 11.8704 7.87934C13.5989 7.87938 15 9.28112 15 11.0095C14.9998 12.7378 13.5988 14.1391 11.8704 14.1391C10.1421 14.1391 8.74104 12.7379 8.74089 11.0095C8.74089 10.5838 8.82585 10.1777 8.97982 9.80772L8 9.08377L7.01953 9.80772C7.17356 10.1778 7.25911 10.5837 7.25911 11.0095C7.25896 12.7379 5.85791 14.1391 4.12956 14.1391C2.40124 14.1391 1.00016 12.7378 1 11.0095C1 9.28112 2.40114 7.87938 4.12956 7.87934C4.95094 7.87934 5.69819 8.19637 6.25651 8.71398L7.33333 7.9197V7.38194C5.92523 7.07639 4.87044 5.82362 4.87044 4.324C4.87048 2.59559 6.27158 1.19444 8 1.19444C9.72842 1.19444 11.1295 2.59559 11.1296 4.324Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095Z" fill="var(--fill-0, #18222F)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 12.9447" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M4.92578 9.8151C4.92578 8.82303 4.12163 8.01823 3.12956 8.01823C2.13752 8.01827 1.33333 8.82306 1.33333 9.8151C1.33349 10.807 2.13762 11.6113 3.12956 11.6113C4.12153 11.6113 4.92563 10.807 4.92578 9.8151ZM12.6667 9.8151C12.6667 8.82306 11.8625 8.01827 10.8704 8.01823C9.87837 8.01823 9.07422 8.82303 9.07422 9.8151C9.07438 10.807 9.87847 11.6113 10.8704 11.6113C11.8624 11.6113 12.6665 10.807 12.6667 9.8151ZM8.79622 3.12956C8.79619 2.13752 7.99205 1.33333 7 1.33333C6.00795 1.33333 5.20382 2.13752 5.20378 3.12956C5.20378 4.12163 6.00793 4.92578 7 4.92578C7.99207 4.92578 8.79622 4.12163 8.79622 3.12956ZM10.1296 3.12956C10.1296 4.62918 9.07477 5.88194 7.66667 6.1875V6.72526L8.74284 7.51953C9.3012 7.00174 10.0489 6.6849 10.8704 6.6849C12.5989 6.68493 14 8.08668 14 9.8151C13.9998 11.5434 12.5988 12.9446 10.8704 12.9447C9.14209 12.9447 7.74104 11.5434 7.74089 9.8151C7.74089 9.38937 7.82585 8.98325 7.97982 8.61328L7 7.88932L6.01953 8.61328C6.17356 8.98332 6.25911 9.38929 6.25911 9.8151C6.25896 11.5434 4.85791 12.9447 3.12956 12.9447C1.40124 12.9446 0.000156326 11.5434 0 9.8151C0 8.08668 1.40114 6.68493 3.12956 6.6849C3.95094 6.6849 4.69819 7.00193 5.25651 7.51953L6.33333 6.72526V6.1875C4.92523 5.88194 3.87044 4.62918 3.87044 3.12956C3.87048 1.40114 5.27158 0 7 0C8.72843 0 10.1295 1.40114 10.1296 3.12956Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.2807" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M12.0014 3.2815L6.33333 0L0.665287 3.2815L6.33333 6.563L12.0014 3.2815ZM0 4.437V11L5.66667 14.2807V7.71767L0 4.437ZM7 14.2807L12.6667 11V4.437L7 7.71767V14.2807Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M6.33333 0L12.6667 3.66667V11L6.33333 14.6667L0 11V3.66667L6.33333 0ZM1.99592 4.0518L6.3334 6.56293L10.6708 4.05183L6.33333 1.54067L1.99592 4.0518ZM1.33333 5.20886V10.2313L5.66673 12.7401V7.71767L1.33333 5.20886ZM7.00007 12.74L11.3333 10.2313V5.20891L7.00007 7.71767V12.74Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M3.33333 2C3.33333 0.895433 4.22877 0 5.33333 0C6.43793 0 7.33333 0.895433 7.33333 2C7.33333 2.23376 7.2932 2.45815 7.21953 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V5.41735C12 5.62345 11.9047 5.81796 11.7418 5.94423C11.5789 6.07053 11.3667 6.11433 11.1671 6.063C11.0079 6.022 10.8403 6 10.6667 6C9.56207 6 8.66667 6.8954 8.66667 8C8.66667 9.1046 9.56207 10 10.6667 10C10.8403 10 11.0079 9.978 11.1671 9.937C11.3667 9.88567 11.5789 9.92947 11.7418 10.0557C11.9047 10.1821 12 10.3765 12 10.5827V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H3.44714C3.37343 2.45815 3.33333 2.23376 3.33333 2Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 940 B |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M2.66667 2.66667C2.66667 1.19391 3.86057 0 5.33333 0C6.80607 0 8 1.19391 8 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V6.1138C12 6.3302 11.8949 6.53313 11.7183 6.65813C11.5415 6.78307 11.3152 6.81447 11.1112 6.74233C10.973 6.69353 10.8237 6.66667 10.6667 6.66667C9.93027 6.66667 9.33333 7.2636 9.33333 8C9.33333 8.7364 9.93027 9.33333 10.6667 9.33333C10.8237 9.33333 10.973 9.30647 11.1112 9.25767C11.3152 9.18553 11.5415 9.21693 11.7183 9.34187C11.8949 9.46687 12 9.6698 12 9.8862V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H2.66667ZM5.33333 1.33333C4.59695 1.33333 4 1.93029 4 2.66667C4 2.82369 4.02687 2.97301 4.0757 3.11117C4.14781 3.31521 4.11641 3.54157 3.99145 3.71826C3.86649 3.89495 3.66355 4 3.44714 4H1.33333V12H10.6667V10.6667C9.19393 10.6667 8 9.47273 8 8C8 6.52727 9.19393 5.33333 10.6667 5.33333V4H7.21953C7.00313 4 6.8002 3.89495 6.6752 3.71826C6.55027 3.54157 6.51887 3.31521 6.591 3.11117C6.6398 2.97301 6.66667 2.8237 6.66667 2.66667C6.66667 1.93029 6.06973 1.33333 5.33333 1.33333Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="13.3333" height="13.3333" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6709 1.83301C12.754 1.83301 12.833 1.90708 12.833 2.00195V9.125L9.125 12.8311L2.00098 12.832C1.90424 12.8318 1.83301 12.7562 1.83301 12.6709V7.16699H2.16699V12.5H8.5V8.66699C8.5 8.57647 8.57647 8.5 8.66699 8.5L12 8.49902H12.5V2.16699H7.16699V1.83301H12.6709ZM11.4473 8.83301H8.83301V12.6533L9.68652 11.7998L11.8008 9.68652L12.6553 8.83203L11.4473 8.83301ZM2.83301 0.5V2.5H4.83301V2.83301H2.83301V4.83301H2.5V2.83301H0.5V2.5H2.5V0.5H2.83301Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 602 B |
@ -0,0 +1,3 @@
|
||||
<svg width="11.6416" height="13.086" viewBox="0 0 11.6416 13.086" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0996 0.512695C10.1337 0.518171 10.156 0.524407 10.168 0.52832C10.2148 0.543681 10.2522 0.579318 10.2705 0.625C10.5581 1.34398 10.5694 1.95842 10.4502 2.44824L10.3955 2.67578L10.5342 2.86328C10.9293 3.39873 11.1416 4.03345 11.1416 4.76172C11.1416 5.94177 10.8789 6.76774 10.4385 7.34668C9.99983 7.92308 9.33952 8.3137 8.42773 8.53711L7.91602 8.66309L8.06738 9.16699C8.1351 9.39201 8.17383 9.65236 8.17383 9.94336L8.16895 11.2803C8.16815 11.4068 8.16748 11.5418 8.16602 11.75L8.16309 12.1553L8.55762 12.2422C8.62114 12.2562 8.67222 12.3062 8.68555 12.3721C8.7036 12.4623 8.64494 12.5503 8.55469 12.5684C8.30873 12.6174 8.13226 12.5563 8.02148 12.4668C7.90812 12.3752 7.83118 12.2287 7.83105 12.043C7.83105 11.9847 7.83103 11.8999 7.83203 11.748C7.83356 11.5396 7.83507 11.4046 7.83594 11.2783C7.83901 10.8059 7.84082 10.3843 7.84082 9.94336C7.84082 9.41961 7.70678 8.93648 7.38281 8.65723C7.27271 8.56225 7.32913 8.38146 7.47363 8.36523C8.5007 8.24979 9.36433 7.98413 9.96094 7.37695C10.5669 6.76005 10.8086 5.884 10.8086 4.76172C10.8086 4.00303 10.5554 3.35601 10.0693 2.82227C10.0264 2.77514 10.0144 2.70773 10.0381 2.64844C10.1874 2.27509 10.24 1.81114 10.126 1.28125L10.0137 0.759766L9.50098 0.905273L9.49414 0.907227C9.09648 1.01979 8.63458 1.25105 8.11035 1.60742C8.06963 1.63503 8.01887 1.64314 7.97168 1.62988C7.37871 1.46312 6.74527 1.37892 6.1084 1.37891C5.4715 1.37891 4.83803 1.46315 4.24512 1.62988C4.19802 1.64313 4.14702 1.63474 4.10645 1.60742C3.57968 1.25283 3.11619 1.02297 2.71777 0.910156L2.20703 0.765625L2.09277 1.28418C1.97674 1.813 2.02979 2.27614 2.17871 2.64844C2.20225 2.70762 2.19033 2.77513 2.14746 2.82227C1.66395 3.35317 1.4082 4.00895 1.4082 4.76172C1.40822 5.88234 1.64983 6.75778 2.25391 7.375C2.84913 7.98307 3.71022 8.25023 4.7334 8.36523C4.87762 8.38143 4.93358 8.56104 4.82422 8.65625C4.66044 8.79875 4.55302 9.0215 4.48828 9.20898C4.41614 9.41793 4.36621 9.67286 4.36621 9.94336V12.043C4.36598 12.3722 4.10732 12.6499 3.64648 12.5693C3.55582 12.5535 3.49488 12.4666 3.51074 12.376C3.52276 12.3083 3.57458 12.2564 3.63965 12.2422L4.0332 12.1562V10.5586L3.49902 10.5947C2.9627 10.6308 2.58298 10.5389 2.30859 10.3555C2.16409 10.2588 2.0263 10.1279 1.83984 9.90527C1.80759 9.86673 1.71954 9.75814 1.64258 9.66211C1.60519 9.61545 1.57219 9.5735 1.55176 9.54785C1.54666 9.54146 1.54226 9.53528 1.53906 9.53125L1.53711 9.5293C1.53789 9.53032 1.54048 9.53427 1.54395 9.53906C1.54454 9.53989 1.55212 9.55023 1.56055 9.56348C1.56664 9.57332 1.58653 9.60864 1.59863 9.63477C1.62166 9.72473 1.52906 9.94237 1.35645 10.1016L1.14648 9.84082L1.53613 9.52734C1.53304 9.52351 1.52837 9.51827 1.52539 9.51465C1.52488 9.51403 1.52438 9.51285 1.52344 9.51172C1.52279 9.51094 1.51991 9.50772 1.5166 9.50391C1.51604 9.50326 1.51446 9.50199 1.5127 9.5C1.21539 9.13323 0.948869 8.85872 0.610352 8.7373C0.523946 8.70626 0.479023 8.61085 0.509766 8.52441C0.540776 8.43794 0.636154 8.39219 0.722656 8.42285C1.0883 8.55402 1.35448 8.77291 1.7793 9.29785C1.78078 9.29986 1.78248 9.30178 1.7832 9.30273C1.78551 9.30578 1.78769 9.30808 1.78809 9.30859C1.79204 9.31367 1.79973 9.32481 1.80859 9.33594C1.82821 9.36058 1.8612 9.401 1.89746 9.44629L2.0957 9.69141C2.2267 9.84782 2.35754 9.98732 2.49316 10.0781C2.78339 10.2725 3.19313 10.2925 3.58887 10.2529L4.01172 10.2109L4.03809 9.78711C4.05126 9.57333 4.09056 9.36582 4.15039 9.17578L4.31055 8.66699L3.79199 8.54004C2.88088 8.31734 2.21997 7.92594 1.78027 7.34863C1.33864 6.76857 1.07521 5.94109 1.0752 4.76172C1.0752 4.0396 1.28837 3.39917 1.68262 2.86328L1.82129 2.67578L1.76562 2.44824C1.64647 1.95845 1.65776 1.34391 1.94531 0.625C1.96354 0.57943 2.00137 0.544774 2.04785 0.529297C2.07893 0.520398 2.08893 0.517862 2.11621 0.513672C2.47591 0.45859 3.10496 0.581722 4.05176 1.1748L4.22852 1.28516L4.43164 1.2373C4.97053 1.11008 5.53743 1.04492 6.1084 1.04492C6.67867 1.04494 7.24479 1.11034 7.7832 1.2373L7.9873 1.28516L8.16504 1.17285C9.10977 0.576084 9.73863 0.454651 10.0996 0.512695ZM2.39551 9.2666C2.28256 9.36997 2.13716 9.45037 1.96484 9.45117C1.91043 9.42066 1.8462 9.37172 1.83301 9.35937C1.82701 9.35346 1.81779 9.34341 1.81445 9.33984C1.81233 9.33754 1.80911 9.3337 1.80762 9.33203C1.80466 9.3287 1.80177 9.32635 1.80078 9.3252L1.79687 9.32031L1.80078 9.32422L2.19043 9.01172C2.22028 9.0493 2.31937 9.17218 2.39551 9.2666Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="13.3333" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.332 0.5C11.426 0.500094 11.5 0.580915 11.5 0.661133V12.6719C11.5 12.7601 11.4282 12.8329 11.3379 12.833H0.662109C0.578569 12.8329 0.5 12.7632 0.5 12.6621V4.20703L4.20898 0.5H11.332ZM4.83301 4.66699C4.83283 4.75878 4.75878 4.83283 4.66699 4.83301H0.833008V12.5H11.167V0.833008H4.83301V4.66699ZM3.64648 1.5332L1.53223 3.64648L0.678711 4.5H4.5V0.680664L3.64648 1.5332Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@ -0,0 +1,3 @@
|
||||
<svg width="14.6667" height="13.3333" viewBox="0 0 14.6667 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.13867 6.72949C8.67785 7.21017 8.03241 7.5 7.33301 7.5C6.63364 7.49991 5.9881 7.21021 5.52734 6.72949L5.16699 6.35254L4.80566 6.72949C4.34483 7.21027 3.69949 7.5 3 7.5C2.90569 7.5 2.81278 7.49447 2.72168 7.48438L2.16699 7.42285V12.5H12.5V7.42285L11.9453 7.48438C11.8543 7.49446 11.7612 7.49999 11.667 7.5C10.9676 7.5 10.3221 7.21017 9.86133 6.72949L9.5 6.35254L9.13867 6.72949ZM2.75977 1.08301L1.14258 3.88379C0.941179 4.21836 0.833008 4.60162 0.833008 5C0.833008 6.19661 1.80338 7.16699 3 7.16699C3.89354 7.16699 4.68499 6.62068 5.01172 5.80566C5.06765 5.66613 5.26532 5.66617 5.32129 5.80566C5.64794 6.62063 6.43956 7.16686 7.33301 7.16699C8.22659 7.16699 9.019 6.62072 9.3457 5.80566C9.4017 5.66633 9.5983 5.66633 9.6543 5.80566C9.981 6.62072 10.7734 7.16699 11.667 7.16699C12.8635 7.16682 13.833 6.1965 13.833 5C13.833 4.59984 13.725 4.21734 13.5186 3.87402H13.5176L11.9072 1.08301L11.7627 0.833008H2.90332L2.75977 1.08301ZM1.83301 7.22754L1.61133 7.0791C0.94019 6.62981 0.5 5.86624 0.5 5C0.5 4.53874 0.625339 4.09665 0.850586 3.72266L0.855469 3.71484L2.66309 0.583008C2.69288 0.531554 2.74815 0.5 2.80762 0.5H11.8594C11.9188 0.500086 11.9742 0.531575 12.0039 0.583008L13.8057 3.7041L13.8096 3.71191C14.0416 4.09734 14.167 4.53978 14.167 5C14.167 5.86614 13.7267 6.62979 13.0557 7.0791L12.833 7.22754V12.5H13.5V12.833H1.16699V12.5H1.83301V7.22754Z" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,6 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.4445 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path d="M7.873 1.01976C8.28503 1.01976 8.68078 1.18055 8.97605 1.46791C9.12039 1.60841 9.23516 1.77638 9.31357 1.96193C9.39199 2.14747 9.43248 2.34683 9.43265 2.54827C9.43282 2.7497 9.39267 2.94913 9.31457 3.13481C9.23647 3.32048 9.12199 3.48865 8.97788 3.62939L4.53331 7.98841C4.48512 8.03528 4.44681 8.09133 4.42065 8.15326C4.39449 8.21519 4.38102 8.28173 4.38102 8.34896C4.38102 8.41619 4.39449 8.48273 4.42065 8.54466C4.44681 8.60659 4.48512 8.66264 4.53331 8.70951C4.63178 8.80538 4.76378 8.85902 4.9012 8.85902C5.03862 8.85902 5.17062 8.80538 5.26908 8.70951L5.32897 8.65024L5.33019 8.64901L9.71304 4.3505C10.0085 4.06393 10.404 3.90381 10.8155 3.90415C11.2271 3.90449 11.6224 4.06527 11.9173 4.35233L11.9479 4.38228C12.0924 4.52293 12.2072 4.69111 12.2856 4.87689C12.3641 5.06267 12.4045 5.26228 12.4045 5.46393C12.4045 5.66559 12.3641 5.8652 12.2856 6.05098C12.2072 6.23676 12.0924 6.40494 11.9479 6.54559L6.62757 11.7632C6.51503 11.8726 6.42557 12.0034 6.36448 12.1479C6.30339 12.2925 6.27191 12.4478 6.27191 12.6047C6.27191 12.7616 6.30339 12.9169 6.36448 13.0615C6.42557 13.206 6.51503 13.3368 6.62757 13.4462L7.72023 14.5175C7.81867 14.6131 7.95053 14.6667 8.08781 14.6667C8.22508 14.6667 8.35695 14.6131 8.45539 14.5175C8.50358 14.4706 8.54189 14.4145 8.56805 14.3526C8.59421 14.2907 8.60769 14.2241 8.60769 14.1569C8.60769 14.0897 8.59421 14.0231 8.56805 13.9612C8.54189 13.8993 8.50358 13.8432 8.45539 13.7964L7.36273 12.7245C7.34667 12.7089 7.33391 12.6902 7.32519 12.6696C7.31647 12.649 7.31198 12.6268 7.31198 12.6044C7.31198 12.582 7.31647 12.5598 7.32519 12.5392C7.33391 12.5186 7.34667 12.4999 7.36273 12.4843L12.683 7.2673C12.924 7.03296 13.1155 6.75268 13.2463 6.44304C13.3771 6.1334 13.4445 5.80068 13.4445 5.46454C13.4445 5.12841 13.3771 4.79569 13.2463 4.48605C13.1155 4.17641 12.924 3.89613 12.683 3.66178L12.6525 3.63123C12.3646 3.35023 12.016 3.13908 11.6336 3.01405C11.2512 2.88903 10.8453 2.85347 10.447 2.91012C10.5038 2.51705 10.4668 2.1161 10.3389 1.74009C10.211 1.36408 9.99593 1.0237 9.71121 0.746808C9.21914 0.267942 8.55962 0 7.873 0C7.18639 0 6.52687 0.267942 6.0348 0.746808L0.152297 6.51564C0.104103 6.56251 0.0657945 6.61857 0.039636 6.6805C0.0134776 6.74243 0 6.80897 0 6.8762C0 6.94342 0.0134776 7.00997 0.039636 7.0719C0.0657945 7.13382 0.104103 7.18988 0.152297 7.23675C0.250735 7.33243 0.382601 7.38595 0.519877 7.38595C0.657153 7.38595 0.789019 7.33243 0.887457 7.23675L6.76996 1.46791C7.06523 1.18055 7.46098 1.01976 7.873 1.01976Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M8.35362 2.74526C8.32747 2.80719 8.28916 2.86324 8.24096 2.91011L3.88989 7.17685C3.74539 7.3175 3.63053 7.48568 3.5521 7.67146C3.47367 7.85724 3.43327 8.05685 3.43327 8.25851C3.43327 8.46016 3.47367 8.65977 3.5521 8.84555C3.63053 9.03133 3.74539 9.19951 3.88989 9.34017C4.18516 9.62753 4.58092 9.78832 4.99294 9.78832C5.40496 9.78832 5.80072 9.62753 6.09598 9.34017L10.4464 5.07343C10.5449 4.97756 10.6769 4.92392 10.8143 4.92392C10.9518 4.92392 11.0837 4.97756 11.1822 5.07343C11.2304 5.1203 11.2687 5.17635 11.2949 5.23828C11.321 5.30021 11.3345 5.36675 11.3345 5.43398C11.3345 5.5012 11.321 5.56775 11.2949 5.62968C11.2687 5.69161 11.2304 5.74766 11.1822 5.79453L6.83114 10.0613C6.339 10.54 5.67951 10.8078 4.99294 10.8078C4.30636 10.8078 3.64688 10.54 3.15473 10.0613C2.91376 9.82692 2.72222 9.54664 2.59143 9.237C2.46064 8.92736 2.39325 8.59464 2.39325 8.25851C2.39325 7.92238 2.46064 7.58966 2.59143 7.28001C2.72222 6.97037 2.91376 6.69009 3.15473 6.45575L7.50519 2.18901C7.60366 2.09315 7.73566 2.03951 7.87308 2.03951C8.0105 2.03951 8.1425 2.09315 8.24096 2.18901C8.28916 2.23588 8.32747 2.29193 8.35362 2.35386C8.37978 2.41579 8.39326 2.48233 8.39326 2.54956C8.39326 2.61679 8.37978 2.68333 8.35362 2.74526Z" fill="var(--fill-0, #495464)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14.5 14.5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M5.08333 0.75V13.75M2.19444 0.75H12.3056C13.1033 0.75 13.75 1.3967 13.75 2.19444V12.3056C13.75 13.1033 13.1033 13.75 12.3056 13.75H2.19444C1.3967 13.75 0.75 13.1033 0.75 12.3056V2.19444C0.75 1.3967 1.3967 0.75 2.19444 0.75Z" stroke="var(--stroke-0, black)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M9.66667 4V0H11.6667C12.0349 0 12.3333 0.29848 12.3333 0.666667V3.33333C12.3333 3.70152 12.0349 4 11.6667 4H9.66667ZM8.33333 13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4H0V2.71625C0 2.47913 0.12594 2.25987 0.330753 2.14039L4 0H8.33333V13.3333Z" fill="var(--fill-0, #18222F)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 528 B |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M11.6667 0C12.0349 0 12.3333 0.29848 12.3333 0.666667V4C12.3333 4.36819 12.0349 4.66667 11.6667 4.66667H8.33333V13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4.66667H0.666667C0.29848 4.66667 0 4.36819 0 4V2.41202C0 2.15951 0.142667 1.92867 0.368527 1.81574L4 0H11.6667ZM8.33333 1.33333H4.31476L1.33333 2.82405V3.33333H5.66667V12.6667H7V3.33333H8.33333V1.33333ZM11 1.33333H9.66667V3.33333H11V1.33333Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@ -0,0 +1,10 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #18222F)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,10 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888ZM8.47852 11.1751L9.43359 9.64779L9.47786 9.58529C9.52536 9.52564 9.5828 9.47422 9.64779 9.43359L11.1751 8.47852L6.81901 6.81901L8.47852 11.1751Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #495464)"/>
|
||||
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #495464)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,9 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.1 11.4333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path d="M1.71667 0C0.768578 0 0 0.768578 0 1.71667C0 2.66476 0.768578 3.43333 1.71667 3.43333H3.71667C4.66475 3.43333 5.43333 2.66476 5.43333 1.71667C5.43333 0.768578 4.66476 0 3.71667 0H1.71667Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M8.38333 4C7.43524 4 6.66667 4.76858 6.66667 5.71667C6.66667 6.66476 7.43524 7.43333 8.38333 7.43333H10.3833C11.3314 7.43333 12.1 6.66476 12.1 5.71667C12.1 4.76858 11.3314 4 10.3833 4H8.38333Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M0 9.71667C0 8.76858 0.768578 8 1.71667 8H3.71667C4.66476 8 5.43333 8.76858 5.43333 9.71667C5.43333 10.6648 4.66475 11.4333 3.71667 11.4333H1.71667C0.768578 11.4333 0 10.6648 0 9.71667Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M7.05001 2.38334H6.38334V1.05001H7.05001C8.15458 1.05001 9.05001 1.94544 9.05001 3.05001H7.71667C7.71667 2.68182 7.4182 2.38334 7.05001 2.38334Z" fill="var(--fill-0, #18222F)"/>
|
||||
<path d="M9.05001 8.38334C9.05001 9.48791 8.15458 10.3833 7.05001 10.3833H6.38334V9.05001H7.05001C7.4182 9.05001 7.71667 8.75153 7.71667 8.38334H9.05001Z" fill="var(--fill-0, #18222F)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,3 @@
|
||||
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Vector" d="M2 12C0.895401 12 7.95946e-07 11.1046 6.99382e-07 10C6.02818e-07 8.89543 0.8954 8 2 8H4C4.8706 8 5.61127 8.55625 5.886 9.33273L6.66667 9.33333C7.4 9.33333 8 8.73333 8 8L7.9994 7.88599C7.22293 7.61125 6.66667 6.8706 6.66667 6C6.66667 5.1294 7.22293 4.38873 7.9994 4.114L8 4C8 3.26667 7.4 2.66667 6.66667 2.66667L5.8862 2.6666C5.61167 3.4434 4.87087 4 4 4L2 4C0.8954 4 9.65672e-08 3.1046 0 2C-9.65672e-08 0.8954 0.8954 2.71413e-07 2 1.74846e-07L4 0C4.8706 -7.61103e-08 5.61127 0.556266 5.886 1.33273L6.66667 1.33333C8.14 1.33333 9.33333 2.52667 9.33333 4L10.6667 4C11.7712 4 12.6667 4.8954 12.6667 6C12.6667 7.1046 11.7712 8 10.6667 8H9.33333C9.33333 9.47276 8.13943 10.6667 6.66667 10.6667L5.8862 10.6666C5.61167 11.4434 4.87087 12 4 12H2ZM2 2.66667L4 2.66667C4.3682 2.66667 4.66667 2.3682 4.66667 2C4.66667 1.6318 4.3682 1.33333 4 1.33333L2 1.33333C1.6318 1.33333 1.33333 1.6318 1.33333 2C1.33333 2.3682 1.6318 2.66667 2 2.66667ZM8.66667 6.66667H10.6667C11.0349 6.66667 11.3333 6.3682 11.3333 6C11.3333 5.6318 11.0349 5.33333 10.6667 5.33333L8.66667 5.33333C8.29848 5.33333 8 5.6318 8 6C8 6.3682 8.29848 6.66667 8.66667 6.66667ZM2 10.6667H4C4.3682 10.6667 4.66667 10.3682 4.66667 10C4.66667 9.63181 4.3682 9.33333 4 9.33333H2C1.6318 9.33333 1.33333 9.63181 1.33333 10C1.33333 10.3682 1.6318 10.6667 2 10.6667Z" fill="var(--fill-0, #495464)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66667 6.17435C2.66667 5.98785 2.66667 5.89459 2.69021 5.80837C2.71107 5.73198 2.74537 5.65992 2.7915 5.59556C2.84357 5.52292 2.91595 5.46411 3.0607 5.3465L7.3274 1.87983C7.56713 1.68502 7.687 1.58762 7.82027 1.55031C7.93787 1.51741 8.06213 1.51741 8.17973 1.55031C8.313 1.58762 8.43287 1.68502 8.6726 1.87983L12.9393 5.3465C13.0841 5.46411 13.1564 5.52292 13.2085 5.59556C13.2547 5.65992 13.2889 5.73198 13.3098 5.80837C13.3333 5.89459 13.3333 5.98785 13.3333 6.17435V12.2667C13.3333 12.64 13.3333 12.8267 13.2607 12.9693C13.1967 13.0947 13.0948 13.1967 12.9693 13.2607C12.8267 13.3333 12.6401 13.3333 12.2667 13.3333H3.73333C3.35997 13.3333 3.17328 13.3333 3.03067 13.2607C2.90523 13.1967 2.80325 13.0947 2.73933 12.9693C2.66667 12.8267 2.66667 12.64 2.66667 12.2667V6.17435Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M10 13.3333V9.66667C10 9.11438 9.55228 8.66667 9 8.66667H7C6.44772 8.66667 6 9.11438 6 9.66667V13.3333" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(3 3) scale(1.5)">
|
||||
<path d="M7 10.5C8.933 10.5 10.5 8.4853 10.5 6C10.5 3.51472 8.933 1.5 7 1.5M7 10.5C5.067 10.5 3.5 8.4853 3.5 6C3.5 3.51472 5.067 1.5 7 1.5M7 10.5H5C3.06701 10.5 1.5 8.4853 1.5 6C1.5 3.51472 3.06701 1.5 5 1.5H7" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15.25C12.4142 15.25 12.75 15.5858 12.75 16V16.0098C12.75 16.424 12.4142 16.7598 12 16.7598C11.5858 16.7598 11.25 16.424 11.25 16.0098V16C11.25 15.5858 11.5858 15.25 12 15.25Z" fill="currentColor"/>
|
||||
<path d="M14 7.25C14.4142 7.25 14.75 7.58579 14.75 8V10.5C14.75 10.7359 14.6389 10.958 14.4502 11.0996L12.75 12.374V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V12C11.25 11.7641 11.3611 11.542 11.5498 11.4004L13.25 10.125V8.75H10.75V9C10.75 9.41421 10.4142 9.75 10 9.75C9.58579 9.75 9.25 9.41421 9.25 9V8C9.25 7.58579 9.58579 7.25 10 7.25H14Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-5.5 -6.95)">
|
||||
<path d="M14.449 8.37314C15.0613 7.87562 15.9387 7.87562 16.551 8.37314L22.3843 13.1127C22.7738 13.4292 23 13.9044 23 14.4063V22.7596C23 23.6801 22.2538 24.4263 21.3333 24.4263H18.8333V17.7599C18.8333 17.2997 18.4602 16.9266 18 16.9266H13C12.5398 16.9266 12.1667 17.2997 12.1667 17.7599V24.4263H9.66667C8.74619 24.4263 8 23.6801 8 22.7596V14.4063C8 13.9044 8.22616 13.4292 8.61568 13.1127L14.449 8.37314Z" fill="currentColor"/>
|
||||
<path d="M13.833 24.4263H17.1663V18.5933H13.833V24.4263Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 666 B |
@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33301 7.71788C3.33301 7.48475 3.33301 7.36818 3.36243 7.2604C3.38851 7.16492 3.43138 7.07484 3.48905 6.99439C3.55414 6.90359 3.64461 6.83008 3.82555 6.68307L9.15892 2.34973C9.45859 2.10622 9.60842 1.98447 9.77501 1.93783C9.92201 1.8967 10.0773 1.8967 10.2243 1.93783C10.3909 1.98447 10.5408 2.10622 10.8404 2.34973L16.1738 6.68307C16.3548 6.83008 16.4452 6.90359 16.5103 6.99439C16.568 7.07484 16.6108 7.16492 16.6369 7.2604C16.6663 7.36818 16.6663 7.48475 16.6663 7.71788V15.3333C16.6663 15.7999 16.6663 16.0334 16.5755 16.2116C16.4956 16.3684 16.3682 16.4959 16.2113 16.5758C16.0331 16.6666 15.7998 16.6666 15.333 16.6666H4.66634C4.19963 16.6666 3.96627 16.6666 3.78802 16.5758C3.63122 16.4959 3.50373 16.3684 3.42383 16.2116C3.33301 16.0334 3.33301 15.7999 3.33301 15.3333V7.71788Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M12.5 16.6666V11.8333C12.5 11.281 12.0523 10.8333 11.5 10.8333H8.5C7.94772 10.8333 7.5 11.281 7.5 11.8333V16.6666" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-6.33 -5.55)">
|
||||
<path d="M9.66602 8.83333C9.66602 8.3731 10.0391 8 10.4993 8H14.666C15.1263 8 15.4993 8.3731 15.4993 8.83333V10.5H9.66602V8.83333Z" fill="currentColor"/>
|
||||
<path d="M17.166 8.83333C17.166 8.3731 17.5391 8 17.9993 8H22.166C22.6263 8 22.9993 8.3731 22.9993 8.83333V10.5H17.166V8.83333Z" fill="currentColor"/>
|
||||
<path d="M8 13.0001C8 12.5398 8.3731 12.1667 8.83333 12.1667H23.8333C24.2936 12.1667 24.6667 12.5398 24.6667 13.0001V21.3334C24.6667 21.7937 24.2936 22.1667 23.8333 22.1667H8.83333C8.3731 22.1667 8 21.7937 8 21.3334V13.0001Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 716 B |
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 7.50008C2.5 7.03984 2.8731 6.66675 3.33333 6.66675H16.6667C17.1269 6.66675 17.5 7.03985 17.5 7.50008V15.0001C17.5 15.4603 17.1269 15.8334 16.6667 15.8334H3.33333C2.8731 15.8334 2.5 15.4603 2.5 15.0001V7.50008Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.16699 6.66659V4.58325C4.16699 3.89289 4.72663 3.33325 5.41699 3.33325H7.08366C7.77402 3.33325 8.33366 3.89289 8.33366 4.58325V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.667 6.66659V4.16659C11.667 3.70635 12.0401 3.33325 12.5003 3.33325H15.0003C15.4606 3.33325 15.8337 3.70635 15.8337 4.16659V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 896 B |
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-4.667 -6.333)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 8C9.11929 8 8 9.11929 8 10.5V22.1667C8 23.5474 9.11929 24.6667 10.5 24.6667H20.5C20.9602 24.6667 21.3333 24.2936 21.3333 23.8333V8.83333C21.3333 8.3731 20.9602 8 20.5 8H10.5ZM9.66667 22.1667C9.66667 22.6269 10.0398 23 10.5 23H19.6667V21.3333H10.5C10.0398 21.3333 9.66667 21.7064 9.66667 22.1667ZM12.1667 11.3333C11.7064 11.3333 11.3333 11.7064 11.3333 12.1667C11.3333 12.6269 11.7064 13 12.1667 13H17.1667C17.6269 13 18 12.6269 18 12.1667C18 11.7064 17.6269 11.3333 17.1667 11.3333H12.1667ZM11.3333 15.5C11.3333 15.0397 11.7064 14.6667 12.1667 14.6667H14.6667C15.1269 14.6667 15.5 15.0397 15.5 15.5C15.5 15.9602 15.1269 16.3333 14.6667 16.3333H12.1667C11.7064 16.3333 11.3333 15.9602 11.3333 15.5Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 933 B |
@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 9.16675H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 5.83325H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.16699 4.16667C4.16699 3.24619 4.91318 2.5 5.83366 2.5H15.417C15.6471 2.5 15.8337 2.68655 15.8337 2.91667V17.5H5.83366C4.91318 17.5 4.16699 16.7538 4.16699 15.8333V4.16667Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M4.16699 15.8334C4.16699 14.9129 4.91318 14.1667 5.83366 14.1667H15.8337V17.5001H5.83366C4.91318 17.5001 4.16699 16.7539 4.16699 15.8334Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 764 B |
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-6.3 -5.5)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25369 8.58477C9.3624 8.23688 9.6846 8 10.0491 8H22.5491C22.9136 8 23.2357 8.23688 23.3445 8.58477L24.4481 12.1162C24.8042 13.256 24.5023 14.4059 23.7991 15.2164V22.1667C23.7991 22.6269 23.426 23 22.9657 23H9.63242C9.17219 23 8.79909 22.6269 8.79909 22.1667V15.2164C8.09588 14.4059 7.79393 13.256 8.15013 12.1162L9.25369 8.58477ZM18.0271 12.7092L17.6467 9.66667H14.9514L14.5711 12.7092C14.4412 13.7486 15.2516 14.6667 16.2991 14.6667C17.3465 14.6667 18.1568 13.7485 18.0271 12.7092ZM13.2718 9.66667H10.6617L9.74093 12.6133C9.42266 13.6317 10.1835 14.6667 11.2505 14.6667C12.0482 14.6667 12.721 14.0728 12.82 13.2812L13.2718 9.66667ZM19.3264 9.66667L19.6809 12.5025L19.7782 13.2812C19.8772 14.0728 20.55 14.6667 21.3477 14.6667C22.4147 14.6667 23.1755 13.6317 22.8572 12.6133L21.9364 9.66667H19.3264Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.667 9.99992V16.6666H3.33366V9.99992M7.91699 3.33325H12.0837M7.91699 3.33325L7.44543 7.10578C7.25334 8.6425 8.45158 9.99992 10.0003 9.99992C11.5491 9.99992 12.7473 8.6425 12.5552 7.10578L12.0837 3.33325M7.91699 3.33325H3.75033L2.64677 6.86465C2.16081 8.41975 3.32257 9.99992 4.95179 9.99992C6.1697 9.99992 7.19703 9.093 7.34809 7.88451L7.91699 3.33325ZM12.0837 3.33325H16.2503L17.3539 6.86465C17.8398 8.41975 16.6781 9.99992 15.0489 9.99992C13.831 9.99992 12.8037 9.093 12.6526 7.88451L12.0837 3.33325Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0004 1.875C17.0398 1.87509 21.1246 5.9602 21.1249 10.9995C21.1249 13.1138 20.4037 15.0582 19.1972 16.6055L21.7958 19.2041C22.235 19.6433 22.2348 20.3556 21.7958 20.7949C21.3565 21.2343 20.6443 21.2343 20.205 20.7949L17.6064 18.1963C16.3417 19.1831 14.8123 19.8466 13.14 20.0552C12.5235 20.132 11.9616 19.6932 11.8847 19.0767C11.8081 18.4603 12.2454 17.8981 12.8617 17.8213C16.2516 17.3983 18.8749 14.5044 18.8749 10.9995C18.8746 7.20283 15.7971 4.12509 12.0004 4.125C8.4954 4.12505 5.60139 6.74948 5.17862 10.1396C5.10154 10.7559 4.53955 11.1934 3.92325 11.1167C3.30688 11.0398 2.86963 10.4777 2.9462 9.86133C3.50765 5.35896 7.34631 1.87505 12.0004 1.875Z" fill="currentColor"/>
|
||||
<path d="M3.70727 16.1747L7.91781 11.2624C8.24038 10.8861 8.85505 11.158 8.79357 11.6498L8.49979 14.0001H10.9127C11.3399 14.0001 11.5703 14.5012 11.2923 14.8255L7.08177 19.7378C6.7592 20.1141 6.14453 19.8422 6.20601 19.3504L6.49979 17.0001H4.0869C3.65972 17.0001 3.42927 16.499 3.70727 16.1747Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,8 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-7.14 -6.38)">
|
||||
<path d="M13.7969 17.1665C14.465 17.1665 15.1067 17.2803 15.7045 17.4872L14.7247 20.9165H12.9636C11.8131 20.9167 10.8803 21.8493 10.8803 22.9998C10.8803 23.2963 10.9436 23.5778 11.0552 23.8332H8.79695C8.33682 23.833 7.95953 23.4587 8.00349 23.0007C8.30296 19.8813 10.2937 17.1666 13.7969 17.1665Z" fill="currentColor"/>
|
||||
<path d="M25.4632 16.3333C25.7246 16.3333 25.9715 16.4558 26.1289 16.6645C26.2668 16.8473 26.3224 17.0777 26.286 17.3008L26.2648 17.3953L24.5981 23.2286C24.4959 23.5863 24.1686 23.8333 23.7965 23.8333H12.9632C12.5031 23.8331 12.1299 23.4601 12.1299 22.9999C12.1299 22.5398 12.5031 22.1668 12.9632 22.1666H15.6675L17.1616 16.9379L17.2105 16.8093C17.3465 16.5223 17.6376 16.3333 17.9632 16.3333H25.4632Z" fill="currentColor"/>
|
||||
<path d="M14.2132 9.25C16.0541 9.25 17.5465 10.7424 17.5465 12.5833C17.5465 14.4243 16.0541 15.9167 14.2132 15.9167C12.3724 15.9165 10.8799 14.4242 10.8799 12.5833C10.8799 10.7425 12.3724 9.25013 14.2132 9.25Z" fill="currentColor"/>
|
||||
<path d="M22.5474 8C22.7539 8.00029 22.9276 8.15533 22.951 8.36052C23.0941 9.62402 23.8103 10.3975 25.093 10.5114C25.3029 10.53 25.4635 10.7067 25.4632 10.9175C25.4628 11.128 25.3019 11.3037 25.0921 11.3219C23.8276 11.4314 23.0613 12.1977 22.9518 13.4622C22.9335 13.672 22.758 13.833 22.5474 13.8333C22.3367 13.8336 22.1609 13.6728 22.1421 13.4631C22.0281 12.1805 21.2546 11.4635 19.9912 11.3203C19.786 11.2971 19.6303 11.124 19.6299 10.9175C19.6297 10.7108 19.7851 10.5367 19.9904 10.513C21.2719 10.3651 21.9951 9.64128 22.1429 8.3597C22.1667 8.15453 22.3408 7.99979 22.5474 8Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z" fill="currentColor"/>
|
||||
<path d="M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2 2.333)">
|
||||
<path d="M1.33333 2.33333C1.33333 1.78105 1.78105 1.33333 2.33333 1.33333C2.88562 1.33333 3.33333 1.78105 3.33333 2.33333C3.33333 2.88562 2.88562 3.33333 2.33333 3.33333C1.78105 3.33333 1.33333 2.88562 1.33333 2.33333ZM2.33333 0C1.04467 0 0 1.04467 0 2.33333C0 3.622 1.04467 4.66667 2.33333 4.66667C3.622 4.66667 4.66667 3.622 4.66667 2.33333C4.66667 1.04467 3.622 0 2.33333 0ZM6 3H11.3333V1.66667H6V3ZM8.66667 9C8.66667 8.44773 9.1144 8 9.66667 8C10.2189 8 10.6667 8.44773 10.6667 9C10.6667 9.55227 10.2189 10 9.66667 10C9.1144 10 8.66667 9.55227 8.66667 9ZM9.66667 6.66667C8.378 6.66667 7.33333 7.71133 7.33333 9C7.33333 10.2887 8.378 11.3333 9.66667 11.3333C10.9553 11.3333 12 10.2887 12 9C12 7.71133 10.9553 6.66667 9.66667 6.66667ZM0.666667 8.33333V9.66667H6V8.33333H0.666667Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 956 B |
@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"name": "Dify Custom Vender",
|
||||
"total": 277,
|
||||
"total": 312,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"./custom-vender/chars.json": "./custom-vender/chars.json"
|
||||
},
|
||||
"scripts": {
|
||||
"check:dimensions": "tsx ./scripts/check-icon-dimensions.ts",
|
||||
"generate": "tsx ./scripts/generate-collections.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
type IconData = {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
type IconCollection = {
|
||||
icons: Record<string, IconData>
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
type DimensionRule = {
|
||||
collection: 'custom-vender'
|
||||
icons: string[]
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const packageDir = path.resolve(__dirname, '..')
|
||||
|
||||
const dimensionRules: DimensionRule[] = [
|
||||
{
|
||||
collection: 'custom-vender',
|
||||
icons: [
|
||||
'main-nav-home',
|
||||
'main-nav-home-active',
|
||||
'main-nav-integrations',
|
||||
'main-nav-integrations-active',
|
||||
'main-nav-knowledge',
|
||||
'main-nav-knowledge-active',
|
||||
'main-nav-marketplace',
|
||||
'main-nav-marketplace-active',
|
||||
'main-nav-studio',
|
||||
'main-nav-studio-active',
|
||||
],
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
]
|
||||
|
||||
const readCollection = async (collection: DimensionRule['collection']): Promise<IconCollection> => {
|
||||
return JSON.parse(
|
||||
await readFile(path.resolve(packageDir, collection, 'icons.json'), 'utf8'),
|
||||
) as IconCollection
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const collections = new Map<string, IconCollection>()
|
||||
const failures: string[] = []
|
||||
|
||||
for (const rule of dimensionRules) {
|
||||
if (!collections.has(rule.collection))
|
||||
collections.set(rule.collection, await readCollection(rule.collection))
|
||||
|
||||
const collection = collections.get(rule.collection)!
|
||||
|
||||
for (const iconName of rule.icons) {
|
||||
const icon = collection.icons[iconName]
|
||||
const width = icon?.width ?? collection.width ?? 16
|
||||
const height = icon?.height ?? collection.height ?? 16
|
||||
|
||||
if (!icon) {
|
||||
failures.push(`${rule.collection}:${iconName} is missing`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (width !== rule.width || height !== rule.height) {
|
||||
failures.push(
|
||||
`${rule.collection}:${iconName} expected ${rule.width}x${rule.height}, got ${width}x${height}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error('Icon dimension check failed:')
|
||||
for (const failure of failures)
|
||||
console.error(`- ${failure}`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Icon dimension check passed.')
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@ -21,6 +21,8 @@ type AliasData = Omit<IconData, 'body'> & {
|
||||
type ImportedCollection = {
|
||||
icons?: Record<string, IconData>
|
||||
aliases?: Record<string, AliasData>
|
||||
width?: number
|
||||
height?: number
|
||||
lastModified?: number
|
||||
}
|
||||
|
||||
@ -60,11 +62,17 @@ const flattenCollections = (collections: ImportedCollections, prefix: string) =>
|
||||
const segment = collectionKey.slice(prefix.length + 1)
|
||||
const namePrefix = segment ? `${segment}-` : ''
|
||||
|
||||
const applyCollectionSize = <T extends IconData | AliasData>(iconData: T): T => ({
|
||||
...iconData,
|
||||
...(iconData.width === undefined && collection.width !== undefined ? { width: collection.width } : {}),
|
||||
...(iconData.height === undefined && collection.height !== undefined ? { height: collection.height } : {}),
|
||||
})
|
||||
|
||||
for (const [iconName, iconData] of Object.entries(collection.icons ?? {}))
|
||||
icons[`${namePrefix}${iconName}`] = iconData
|
||||
icons[`${namePrefix}${iconName}`] = applyCollectionSize(iconData)
|
||||
|
||||
for (const [aliasName, aliasData] of Object.entries(collection.aliases ?? {}))
|
||||
aliases[`${namePrefix}${aliasName}`] = aliasData
|
||||
aliases[`${namePrefix}${aliasName}`] = applyCollectionSize(aliasData)
|
||||
|
||||
if (typeof collection.lastModified === 'number')
|
||||
lastModified = Math.max(lastModified, collection.lastModified)
|
||||
|
||||
16
pnpm-lock.yaml
generated
@ -300,6 +300,9 @@ catalogs:
|
||||
embla-carousel-autoplay:
|
||||
specifier: 8.6.0
|
||||
version: 8.6.0
|
||||
embla-carousel-fade:
|
||||
specifier: 8.6.0
|
||||
version: 8.6.0
|
||||
embla-carousel-react:
|
||||
specifier: 8.6.0
|
||||
version: 8.6.0
|
||||
@ -1013,6 +1016,9 @@ importers:
|
||||
embla-carousel-autoplay:
|
||||
specifier: 'catalog:'
|
||||
version: 8.6.0(embla-carousel@8.6.0)
|
||||
embla-carousel-fade:
|
||||
specifier: 'catalog:'
|
||||
version: 8.6.0(embla-carousel@8.6.0)
|
||||
embla-carousel-react:
|
||||
specifier: 'catalog:'
|
||||
version: 8.6.0(react@19.2.6)
|
||||
@ -5345,6 +5351,11 @@ packages:
|
||||
peerDependencies:
|
||||
embla-carousel: 8.6.0
|
||||
|
||||
embla-carousel-fade@8.6.0:
|
||||
resolution: {integrity: sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==}
|
||||
peerDependencies:
|
||||
embla-carousel: 8.6.0
|
||||
|
||||
embla-carousel-react@8.6.0:
|
||||
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
||||
peerDependencies:
|
||||
@ -12412,6 +12423,10 @@ snapshots:
|
||||
dependencies:
|
||||
embla-carousel: 8.6.0
|
||||
|
||||
embla-carousel-fade@8.6.0(embla-carousel@8.6.0):
|
||||
dependencies:
|
||||
embla-carousel: 8.6.0
|
||||
|
||||
embla-carousel-react@8.6.0(react@19.2.6):
|
||||
dependencies:
|
||||
embla-carousel: 8.6.0
|
||||
@ -16280,6 +16295,7 @@ time:
|
||||
echarts@6.0.0: '2025-07-30T02:38:34.897Z'
|
||||
elkjs@0.11.1: '2026-03-03T12:21:48.463Z'
|
||||
embla-carousel-autoplay@8.6.0: '2025-04-04T17:37:46.303Z'
|
||||
embla-carousel-fade@8.6.0: '2025-04-04T17:37:50.278Z'
|
||||
embla-carousel-react@8.6.0: '2025-04-04T17:37:53.976Z'
|
||||
emoji-mart@5.6.0: '2024-04-25T14:22:21.440Z'
|
||||
es-toolkit@1.46.1: '2026-04-29T09:42:09.686Z'
|
||||
|
||||
@ -157,6 +157,7 @@ catalog:
|
||||
echarts-for-react: 3.0.6
|
||||
elkjs: 0.11.1
|
||||
embla-carousel-autoplay: 8.6.0
|
||||
embla-carousel-fade: 8.6.0
|
||||
embla-carousel-react: 8.6.0
|
||||
emoji-mart: 5.6.0
|
||||
es-toolkit: 1.46.1
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
## Frontend Workflow
|
||||
|
||||
- Refer to the `./docs/test.md` and `./docs/lint.md` for detailed frontend workflow instructions.
|
||||
- For frontend coding tasks, also apply the repo-local `how-to-write-component` and `tailwind-css-rules` skills when the change touches React components, state ownership, routing, styling, or Tailwind classes.
|
||||
- For frontend reviews, use the repo-local `frontend-code-review` skill as the canonical checklist.
|
||||
|
||||
## i18n
|
||||
|
||||
- User-facing strings must use `web/i18n/en-US/` keys instead of hardcoded text.
|
||||
- When adding or renaming an i18n key, update all supported locale files with correct localized values; do not leave fallback English in non-English locales unless the repo already intentionally does so for that exact key.
|
||||
|
||||
## Overlay Components (Mandatory)
|
||||
|
||||
@ -9,6 +16,14 @@
|
||||
- In new or modified code, use only overlay primitives from `@langgenius/dify-ui/*`.
|
||||
- Do not introduce overlay imports from `@/app/components/base/*`; when touching existing callers, migrate them.
|
||||
|
||||
## SVG Icons (Mandatory)
|
||||
|
||||
- New custom SVG icons must be added under `../packages/iconify-collections/assets/...`.
|
||||
- Run `pnpm --filter @dify/iconify-collections generate` and consume generated icons with Tailwind `i-custom-*` classes.
|
||||
- Restart the web dev server after regenerating icons because Tailwind loads the custom icon collection at startup.
|
||||
- Do not add new generated React icon components or JSON files under `app/components/base/icons/src/...`.
|
||||
- See `../packages/iconify-collections/README.md` for the full workflow.
|
||||
|
||||
## Design Token Mapping
|
||||
|
||||
- When translating Figma designs to code, read `../packages/dify-ui/AGENTS.md` for the Figma `--radius/*` token to Tailwind `rounded-*` class mapping. The two scales are offset by one step.
|
||||
|
||||
@ -1,30 +1,15 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/nav-link'
|
||||
import type { App } from '@/types/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiDashboard2Fill,
|
||||
RiDashboard2Line,
|
||||
RiFileList3Fill,
|
||||
RiFileList3Line,
|
||||
RiTerminalBoxFill,
|
||||
RiTerminalBoxLine,
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import { AppInfoDetailLayer } from '@/app/components/app-sidebar/app-info'
|
||||
import { useAppInfoActions } from '@/app/components/app-sidebar/app-info/use-app-info-actions'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
@ -36,6 +21,13 @@ type IAppDetailLayoutProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const isNotFoundError = (error: unknown) => (
|
||||
typeof error === 'object'
|
||||
&& error !== null
|
||||
&& 'status' in error
|
||||
&& error.status === 404
|
||||
)
|
||||
|
||||
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
@ -44,91 +36,34 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace } = useAppContext()
|
||||
const appInfoActions = useAppInfoActions({ resetKey: appId })
|
||||
const { appDetail, setAppDetail, setAppSidebarExpand } = useStore(useShallow(state => ({
|
||||
const { appDetail, setAppDetail } = useStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
setAppDetail: state.setAppDetail,
|
||||
setAppSidebarExpand: state.setAppSidebarExpand,
|
||||
})))
|
||||
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
|
||||
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
|
||||
const [navigation, setNavigation] = useState<Array<{
|
||||
name: string
|
||||
href: string
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
}>>([])
|
||||
|
||||
const getNavigationConfig = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: AppModeEnum) => {
|
||||
const navConfig = [
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: t('appMenus.promptEng', { ns: 'common' }),
|
||||
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.apiAccess', { ns: 'common' }),
|
||||
href: `/app/${appId}/develop`,
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
},
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: mode !== AppModeEnum.WORKFLOW
|
||||
? t('appMenus.logAndAnn', { ns: 'common' })
|
||||
: t('appMenus.logs', { ns: 'common' }),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
},
|
||||
]
|
||||
return navConfig
|
||||
}, [t])
|
||||
|
||||
useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' }))
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
// TODO: consider screen size and mode
|
||||
// if ((appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
||||
// setAppSidebarExpand('collapse')
|
||||
}
|
||||
}, [appDetail, isMobile])
|
||||
|
||||
useEffect(() => {
|
||||
setAppDetail()
|
||||
setIsLoadingAppDetail(true)
|
||||
void Promise.resolve().then(() => setIsLoadingAppDetail(true))
|
||||
fetchAppDetailDirect({ url: '/apps', id: appId }).then((res: App) => {
|
||||
setAppDetailRes(res)
|
||||
}).catch((e: any) => {
|
||||
if (e.status === 404)
|
||||
}).catch((error: unknown) => {
|
||||
if (isNotFoundError(error))
|
||||
router.replace('/apps')
|
||||
}).finally(() => {
|
||||
setIsLoadingAppDetail(false)
|
||||
})
|
||||
}, [appId, pathname])
|
||||
}, [appId, pathname, router, setAppDetail])
|
||||
|
||||
useEffect(() => {
|
||||
if (!appDetailRes || !currentWorkspace.id || isLoadingCurrentWorkspace || isLoadingAppDetail)
|
||||
return
|
||||
if (appDetailRes.id !== appId)
|
||||
return
|
||||
const res = appDetailRes
|
||||
// redirection
|
||||
const canIEditApp = isCurrentWorkspaceEditor
|
||||
@ -144,9 +79,8 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
}
|
||||
else {
|
||||
setAppDetail({ ...res, enable_sso: false })
|
||||
setNavigation(getNavigationConfig(appId, isCurrentWorkspaceEditor, res.mode))
|
||||
}
|
||||
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace])
|
||||
}, [appDetailRes, appId, currentWorkspace.id, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail])
|
||||
|
||||
useUnmount(() => {
|
||||
setAppDetail()
|
||||
@ -162,16 +96,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div className={cn(s.app, 'relative flex', 'overflow-hidden')}>
|
||||
{appDetail && (
|
||||
<AppSideBar
|
||||
navigation={navigation}
|
||||
appInfoActions={appInfoActions}
|
||||
/>
|
||||
)}
|
||||
<div className="grow overflow-hidden bg-components-panel-bg">
|
||||
{children}
|
||||
</div>
|
||||
<AppInfoDetailLayer actions={appInfoActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import { useDatasetDetail } from '@/service/knowledge/use-dataset'
|
||||
import DatasetDetailLayout from '../layout-main'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
const mockSetAppSidebarExpand = vi.fn()
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: vi.fn(),
|
||||
@ -13,13 +12,6 @@ vi.mock('@/next/navigation', () => ({
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useDatasetDetail: vi.fn(),
|
||||
useDatasetRelatedApps: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({
|
||||
setAppSidebarExpand: mockSetAppSidebarExpand,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
@ -34,29 +26,13 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => 'desktop',
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app-sidebar', () => ({
|
||||
default: () => <aside aria-label="dataset navigation" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/extra-info', () => ({
|
||||
default: () => <div />,
|
||||
}))
|
||||
|
||||
const mockUsePathname = vi.mocked(usePathname)
|
||||
const mockUseRouter = vi.mocked(useRouter)
|
||||
const mockUseDatasetDetail = vi.mocked(useDatasetDetail)
|
||||
const mockUseDatasetRelatedApps = vi.mocked(useDatasetRelatedApps)
|
||||
|
||||
describe('DatasetDetailLayout', () => {
|
||||
beforeEach(() => {
|
||||
@ -70,7 +46,6 @@ describe('DatasetDetailLayout', () => {
|
||||
replace: mockReplace,
|
||||
prefetch: vi.fn(),
|
||||
})
|
||||
mockUseDatasetRelatedApps.mockReturnValue({ data: undefined } as ReturnType<typeof useDatasetRelatedApps>)
|
||||
})
|
||||
|
||||
describe('Access Errors', () => {
|
||||
@ -93,7 +68,6 @@ describe('DatasetDetailLayout', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||
})
|
||||
expect(mockUseDatasetRelatedApps).toHaveBeenCalledWith('dataset-1', { enabled: false })
|
||||
expect(screen.queryByText('Pipeline content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -144,7 +118,6 @@ describe('DatasetDetailLayout', () => {
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Pipeline content')).toBeInTheDocument()
|
||||
expect(mockUseDatasetRelatedApps).toHaveBeenCalledWith('dataset-1', { enabled: true })
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,30 +1,15 @@
|
||||
'use client'
|
||||
import type { RemixiconComponentType } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiEqualizer2Fill,
|
||||
RiEqualizer2Line,
|
||||
RiFileTextFill,
|
||||
RiFileTextLine,
|
||||
RiFocus2Fill,
|
||||
RiFocus2Line,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import ExtraInfo from '@/app/components/datasets/extra-info'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import { useDatasetDetail } from '@/service/knowledge/use-dataset'
|
||||
|
||||
type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
@ -52,84 +37,28 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline')
|
||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
|
||||
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
|
||||
const [hideHeader, setHideHeader] = useState(() => localStorage.getItem('workflow-canvas-maximize') === 'true')
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v?.type === 'workflow-canvas-maximize')
|
||||
eventEmitter?.useSubscription((v: unknown) => {
|
||||
if (
|
||||
typeof v === 'object'
|
||||
&& v !== null
|
||||
&& 'type' in v
|
||||
&& v.type === 'workflow-canvas-maximize'
|
||||
&& 'payload' in v
|
||||
&& typeof v.payload === 'boolean'
|
||||
) {
|
||||
setHideHeader(v.payload)
|
||||
}
|
||||
})
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const { data: datasetRes, error, refetch: mutateDatasetRes } = useDatasetDetail(datasetId)
|
||||
const shouldRedirect = shouldRedirectToDatasetList(error)
|
||||
|
||||
const { data: relatedApps } = useDatasetRelatedApps(datasetId, { enabled: !!datasetRes && !shouldRedirect })
|
||||
|
||||
const isButtonDisabledWithPipeline = useMemo(() => {
|
||||
if (!datasetRes)
|
||||
return true
|
||||
if (datasetRes.provider === 'external')
|
||||
return false
|
||||
if (datasetRes.runtime_mode === 'general')
|
||||
return false
|
||||
return !datasetRes.is_published
|
||||
}, [datasetRes])
|
||||
|
||||
const navigation = useMemo(() => {
|
||||
const baseNavigation = [
|
||||
{
|
||||
name: t('datasetMenus.hitTesting', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/hitTesting`,
|
||||
icon: RiFocus2Line,
|
||||
selectedIcon: RiFocus2Fill,
|
||||
disabled: isButtonDisabledWithPipeline,
|
||||
},
|
||||
{
|
||||
name: t('datasetMenus.settings', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/settings`,
|
||||
icon: RiEqualizer2Line,
|
||||
selectedIcon: RiEqualizer2Fill,
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
|
||||
if (datasetRes?.provider !== 'external') {
|
||||
baseNavigation.unshift({
|
||||
name: t('datasetMenus.pipeline', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/pipeline`,
|
||||
icon: PipelineLine as RemixiconComponentType,
|
||||
selectedIcon: PipelineFill as RemixiconComponentType,
|
||||
disabled: false,
|
||||
})
|
||||
baseNavigation.unshift({
|
||||
name: t('datasetMenus.documents', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/documents`,
|
||||
icon: RiFileTextLine,
|
||||
selectedIcon: RiFileTextFill,
|
||||
disabled: isButtonDisabledWithPipeline,
|
||||
})
|
||||
}
|
||||
|
||||
return baseNavigation
|
||||
}, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider])
|
||||
|
||||
useDocumentTitle(datasetRes?.name || t('menus.datasets', { ns: 'common' }))
|
||||
|
||||
const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand)
|
||||
|
||||
useEffect(() => {
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
}, [isMobile, setAppSidebarExpand])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRedirect)
|
||||
router.replace('/datasets')
|
||||
@ -154,17 +83,6 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
mutateDatasetRes,
|
||||
}}
|
||||
>
|
||||
{!hideSideBar && (
|
||||
<AppSideBar
|
||||
navigation={navigation}
|
||||
extraInfo={
|
||||
!isCurrentWorkspaceDatasetOperator
|
||||
? mode => <ExtraInfo relatedApps={relatedApps} expand={mode === 'expand'} documentCount={datasetRes?.document_count} />
|
||||
: undefined
|
||||
}
|
||||
iconType="dataset"
|
||||
/>
|
||||
)}
|
||||
<div className="grow overflow-hidden bg-background-default-subtle">{children}</div>
|
||||
</DatasetDetailContext.Provider>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,23 @@
|
||||
import * as React from 'react'
|
||||
import AppList from '@/app/components/explore/app-list'
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
const Apps = () => {
|
||||
return <AppList />
|
||||
type AppsPageProps = {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
export default React.memo(Apps)
|
||||
const Apps = async ({ searchParams }: AppsPageProps) => {
|
||||
const resolvedSearchParams = await searchParams
|
||||
const urlSearchParams = new URLSearchParams()
|
||||
Object.entries(resolvedSearchParams).forEach(([key, value]) => {
|
||||
if (value === undefined)
|
||||
return
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => urlSearchParams.append(key, item))
|
||||
return
|
||||
}
|
||||
urlSearchParams.set(key, value)
|
||||
})
|
||||
const queryString = urlSearchParams.toString()
|
||||
redirect(queryString ? `/?${queryString}` : '/')
|
||||
}
|
||||
|
||||
export default Apps
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import PageUnavailable from '@/app/components/base/page-unavailable'
|
||||
|
||||
const IntegrationsNotFound = () => {
|
||||
return <PageUnavailable className="h-full w-full bg-components-panel-bg" />
|
||||
}
|
||||
|
||||
export default IntegrationsNotFound
|
||||
26
web/app/(commonLayout)/integrations/[[...slug]]/page.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { getIntegrationRouteTargetBySlug } from '@/app/components/tools/integration-routes'
|
||||
import IntegrationsPage from '@/app/components/tools/integrations-page'
|
||||
import { notFound, redirect } from '@/next/navigation'
|
||||
|
||||
type IntegrationsRoutePageProps = {
|
||||
params: Promise<{
|
||||
slug?: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
const IntegrationsRoutePage = async ({
|
||||
params,
|
||||
}: IntegrationsRoutePageProps) => {
|
||||
const { slug } = await params
|
||||
const target = getIntegrationRouteTargetBySlug(slug)
|
||||
|
||||
if (target.type === 'redirect')
|
||||
redirect(target.destination)
|
||||
|
||||
if (target.type === 'not-found')
|
||||
notFound()
|
||||
|
||||
return <IntegrationsPage section={target.section} />
|
||||
}
|
||||
|
||||
export default IntegrationsRoutePage
|
||||
12
web/app/(commonLayout)/integrations/layout.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
export default function IntegrationsLayout({ children }: PropsWithChildren) {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('mainNav.integrations', { ns: 'common' }))
|
||||
|
||||
return children
|
||||
}
|
||||
@ -6,8 +6,7 @@ import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import Zendesk from '@/app/components/base/zendesk'
|
||||
import { GotoAnything } from '@/app/components/goto-anything'
|
||||
import Header from '@/app/components/header'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import MainNavLayout from '@/app/components/main-nav/layout'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import { AppContextProvider } from '@/context/app-context-provider'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
@ -26,12 +25,11 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<RoleRouteGuard>
|
||||
{children}
|
||||
</RoleRouteGuard>
|
||||
<MainNavLayout>
|
||||
<RoleRouteGuard>
|
||||
{children}
|
||||
</RoleRouteGuard>
|
||||
</MainNavLayout>
|
||||
<InSiteMessageNotification />
|
||||
<PartnerStack />
|
||||
<ReadmePanel />
|
||||
|
||||
18
web/app/(commonLayout)/marketplace/page.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import type { SearchParams } from 'nuqs'
|
||||
import Marketplace from '@/app/components/plugins/marketplace'
|
||||
|
||||
type MarketplacePageProps = {
|
||||
searchParams?: Promise<SearchParams>
|
||||
}
|
||||
|
||||
const MarketplacePage = ({
|
||||
searchParams,
|
||||
}: MarketplacePageProps) => {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-body pt-8">
|
||||
<Marketplace searchParams={searchParams} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketplacePage
|
||||
15
web/app/(commonLayout)/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppList from '@/app/components/explore/app-list'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
const Home = () => {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('mainNav.home', { ns: 'common' }))
|
||||
|
||||
return <AppList />
|
||||
}
|
||||
|
||||
export default React.memo(Home)
|
||||
@ -1,8 +1,22 @@
|
||||
import type { LegacyPluginsSearchParams } from '@/app/components/plugins/plugin-routes'
|
||||
import Marketplace from '@/app/components/plugins/marketplace'
|
||||
import PluginPage from '@/app/components/plugins/plugin-page'
|
||||
import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
|
||||
import { getLegacyPluginRedirectPath } from '@/app/components/plugins/plugin-routes'
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
type PluginListProps = {
|
||||
searchParams?: Promise<LegacyPluginsSearchParams>
|
||||
}
|
||||
|
||||
const PluginList = async ({
|
||||
searchParams,
|
||||
}: PluginListProps) => {
|
||||
const redirectPath = getLegacyPluginRedirectPath(await searchParams)
|
||||
|
||||
if (redirectPath)
|
||||
redirect(redirectPath)
|
||||
|
||||
const PluginList = () => {
|
||||
return (
|
||||
<PluginPage
|
||||
plugins={<PluginsPanel />}
|
||||
|
||||
@ -57,7 +57,13 @@ describe('RoleRouteGuard', () => {
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should redirect dataset operator on guarded routes', async () => {
|
||||
it.each([
|
||||
'/',
|
||||
'/apps',
|
||||
'/tools',
|
||||
'/integrations/model-provider',
|
||||
])('should redirect dataset operator on guarded route %s', async (pathname) => {
|
||||
mockPathname = pathname
|
||||
setAppContext({
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
})
|
||||
|
||||
@ -6,7 +6,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
|
||||
const datasetOperatorRedirectRoutes = ['/apps', '/app', '/explore', '/tools'] as const
|
||||
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/explore', '/tools', '/integrations'] as const
|
||||
|
||||
const isPathUnderRoute = (pathname: string, route: string) => pathname === route || pathname.startsWith(`${route}/`)
|
||||
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ToolProviderList from '@/app/components/tools/provider-list'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import type { LegacyToolsSearchParams } from '@/app/components/tools/integration-routes'
|
||||
import { getIntegrationRedirectPathByLegacyToolsSearchParams } from '@/app/components/tools/integration-routes'
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
const ToolsList: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('menus.tools', { ns: 'common' }))
|
||||
|
||||
return <ToolProviderList />
|
||||
type ToolsPageProps = {
|
||||
searchParams?: Promise<LegacyToolsSearchParams>
|
||||
}
|
||||
export default React.memo(ToolsList)
|
||||
|
||||
const ToolsPage = async ({
|
||||
searchParams,
|
||||
}: ToolsPageProps) => {
|
||||
const resolvedSearchParams = await searchParams
|
||||
|
||||
redirect(getIntegrationRedirectPathByLegacyToolsSearchParams(resolvedSearchParams))
|
||||
}
|
||||
|
||||
export default ToolsPage
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
|
||||
import AppDetailTop from '../app-detail-top'
|
||||
|
||||
const mockBack = vi.fn()
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
back: mockBack,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('AppDetailTop', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('links the home icon to home instead of studio', () => {
|
||||
render(<AppDetailTop />)
|
||||
|
||||
expect(screen.getByRole('link', { name: 'common.mainNav.home' })).toHaveAttribute('href', '/')
|
||||
})
|
||||
|
||||
it('keeps the back button and quick search actions', () => {
|
||||
const handleOpen = vi.fn()
|
||||
window.addEventListener(GOTO_ANYTHING_OPEN_EVENT, handleOpen)
|
||||
|
||||
render(<AppDetailTop />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.back' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.gotoAnything.searchTitle' }))
|
||||
|
||||
expect(mockBack).toHaveBeenCalledTimes(1)
|
||||
expect(handleOpen).toHaveBeenCalledTimes(1)
|
||||
|
||||
window.removeEventListener(GOTO_ANYTHING_OPEN_EVENT, handleOpen)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,38 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
|
||||
import DatasetDetailTop from '../dataset-detail-top'
|
||||
|
||||
const mockBack = vi.fn()
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
back: mockBack,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('DatasetDetailTop', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('links the home icon to home and labels the breadcrumb as datasets', () => {
|
||||
render(<DatasetDetailTop />)
|
||||
|
||||
expect(screen.getByRole('link', { name: 'common.mainNav.home' })).toHaveAttribute('href', '/')
|
||||
expect(screen.getByText('common.menus.datasets')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the back button and quick search actions', () => {
|
||||
const handleOpen = vi.fn()
|
||||
window.addEventListener(GOTO_ANYTHING_OPEN_EVENT, handleOpen)
|
||||
|
||||
render(<DatasetDetailTop />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.back' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.gotoAnything.searchTitle' }))
|
||||
|
||||
expect(mockBack).toHaveBeenCalledTimes(1)
|
||||
expect(handleOpen).toHaveBeenCalledTimes(1)
|
||||
|
||||
window.removeEventListener(GOTO_ANYTHING_OPEN_EVENT, handleOpen)
|
||||
})
|
||||
})
|
||||
116
web/app/components/app-sidebar/app-detail-section.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import type { NavIcon } from './nav-link'
|
||||
import {
|
||||
RiDashboard2Fill,
|
||||
RiDashboard2Line,
|
||||
RiFileList3Fill,
|
||||
RiFileList3Line,
|
||||
RiTerminalBoxFill,
|
||||
RiTerminalBoxLine,
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { AppInfoView } from './app-info'
|
||||
import { useAppInfoActions } from './app-info/use-app-info-actions'
|
||||
import NavLink from './nav-link'
|
||||
|
||||
type AppDetailNavItem = {
|
||||
name: string
|
||||
href: string
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
}
|
||||
|
||||
const AppDetailSection = () => {
|
||||
const { t } = useTranslation()
|
||||
const pathname = usePathname()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const appDetail = useStore(state => state.appDetail)
|
||||
const appInfoActions = useAppInfoActions({ resetKey: appDetail?.id })
|
||||
|
||||
const navigation = useMemo<AppDetailNavItem[]>(() => {
|
||||
if (!appDetail)
|
||||
return []
|
||||
|
||||
const appId = appDetail.id
|
||||
const isWorkflowApp = appDetail.mode === AppModeEnum.WORKFLOW || appDetail.mode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
return [
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: t('appMenus.promptEng', { ns: 'common' }),
|
||||
href: `/app/${appId}/${isWorkflowApp ? 'workflow' : 'configuration'}`,
|
||||
icon: RiTerminalWindowLine,
|
||||
selectedIcon: RiTerminalWindowFill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.apiAccess', { ns: 'common' }),
|
||||
href: `/app/${appId}/develop`,
|
||||
icon: RiTerminalBoxLine,
|
||||
selectedIcon: RiTerminalBoxFill,
|
||||
},
|
||||
...(isCurrentWorkspaceEditor
|
||||
? [{
|
||||
name: appDetail.mode !== AppModeEnum.WORKFLOW
|
||||
? t('appMenus.logAndAnn', { ns: 'common' })
|
||||
: t('appMenus.logs', { ns: 'common' }),
|
||||
href: `/app/${appId}/logs`,
|
||||
icon: RiFileList3Line,
|
||||
selectedIcon: RiFileList3Fill,
|
||||
}]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: t('appMenus.overview', { ns: 'common' }),
|
||||
href: `/app/${appId}/overview`,
|
||||
icon: RiDashboard2Line,
|
||||
selectedIcon: RiDashboard2Fill,
|
||||
},
|
||||
]
|
||||
}, [appDetail, isCurrentWorkspaceEditor, t])
|
||||
|
||||
if (!appDetail)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col px-2 pb-2">
|
||||
<div className="py-2">
|
||||
<AppInfoView
|
||||
expand
|
||||
actions={appInfoActions}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-2">
|
||||
<Divider
|
||||
type="horizontal"
|
||||
bgStyle="gradient"
|
||||
className="my-0 h-px bg-linear-to-r from-divider-subtle to-background-gradient-mask-transparent"
|
||||
/>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-y-0.5 px-1 py-2">
|
||||
{navigation.map(item => (
|
||||
<NavLink
|
||||
key={item.href}
|
||||
mode="expand"
|
||||
iconMap={{ selected: item.selectedIcon, normal: item.icon }}
|
||||
name={item.name}
|
||||
href={item.href}
|
||||
pathname={pathname}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppDetailSection
|
||||
54
web/app/components/app-sidebar/app-detail-top.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
|
||||
const AppDetailTop = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-3 pr-3 pl-1">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<div className="flex shrink-0 items-center py-1 pr-1 pl-0.5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.back', { ns: 'common' })}
|
||||
className="flex size-4 items-center justify-center text-text-tertiary hover:text-text-secondary"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<span aria-hidden className="i-ri-arrow-left-s-line size-4" />
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
aria-label={t('mainNav.home', { ns: 'common' })}
|
||||
className="flex size-4 items-center justify-center text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-main-nav-app-home size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<span className="mx-1.5 shrink-0 system-md-regular text-text-quaternary">
|
||||
/
|
||||
</span>
|
||||
<span className="shrink-0 truncate system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('menus.apps', { ns: 'common' })}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('gotoAnything.searchTitle', { ns: 'app' })}
|
||||
className="flex shrink-0 items-center gap-1 overflow-hidden rounded-[10px] p-1 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={() => window.dispatchEvent(new Event(GOTO_ANYTHING_OPEN_EVENT))}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-main-nav-quick-search size-4" />
|
||||
<span className="rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
⌘K
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppDetailTop
|
||||
134
web/app/components/app-sidebar/dataset-detail-section.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import type { RemixiconComponentType } from '@remixicon/react'
|
||||
import {
|
||||
RiEqualizer2Fill,
|
||||
RiEqualizer2Line,
|
||||
RiFileTextFill,
|
||||
RiFileTextLine,
|
||||
RiFocus2Fill,
|
||||
RiFocus2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import ExtraInfo from '@/app/components/datasets/extra-info'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
|
||||
import DatasetInfo from './dataset-info'
|
||||
import NavLink from './nav-link'
|
||||
|
||||
const getDatasetIdFromPathname = (pathname: string) => {
|
||||
const [, section, datasetId] = pathname.split('/')
|
||||
return section === 'datasets' ? datasetId : undefined
|
||||
}
|
||||
|
||||
const DatasetDetailSection = () => {
|
||||
const { t } = useTranslation()
|
||||
const pathname = usePathname()
|
||||
const datasetId = getDatasetIdFromPathname(pathname)
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { data: datasetRes, refetch: mutateDatasetRes } = useDatasetDetail(datasetId ?? '')
|
||||
const { data: relatedApps } = useDatasetRelatedApps(datasetId ?? '', { enabled: !!datasetId && !!datasetRes })
|
||||
|
||||
const isButtonDisabledWithPipeline = useMemo(() => {
|
||||
if (!datasetRes)
|
||||
return true
|
||||
if (datasetRes.provider === 'external')
|
||||
return false
|
||||
if (datasetRes.runtime_mode === 'general')
|
||||
return false
|
||||
return !datasetRes.is_published
|
||||
}, [datasetRes])
|
||||
|
||||
const navigation = useMemo(() => {
|
||||
if (!datasetId)
|
||||
return []
|
||||
|
||||
const baseNavigation = [
|
||||
{
|
||||
name: t('datasetMenus.hitTesting', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/hitTesting`,
|
||||
icon: RiFocus2Line,
|
||||
selectedIcon: RiFocus2Fill,
|
||||
disabled: isButtonDisabledWithPipeline,
|
||||
},
|
||||
{
|
||||
name: t('datasetMenus.settings', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/settings`,
|
||||
icon: RiEqualizer2Line,
|
||||
selectedIcon: RiEqualizer2Fill,
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
|
||||
if (datasetRes?.provider !== 'external') {
|
||||
baseNavigation.unshift({
|
||||
name: t('datasetMenus.pipeline', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/pipeline`,
|
||||
icon: PipelineLine as RemixiconComponentType,
|
||||
selectedIcon: PipelineFill as RemixiconComponentType,
|
||||
disabled: false,
|
||||
})
|
||||
baseNavigation.unshift({
|
||||
name: t('datasetMenus.documents', { ns: 'common' }),
|
||||
href: `/datasets/${datasetId}/documents`,
|
||||
icon: RiFileTextLine,
|
||||
selectedIcon: RiFileTextFill,
|
||||
disabled: isButtonDisabledWithPipeline,
|
||||
})
|
||||
}
|
||||
|
||||
return baseNavigation
|
||||
}, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider])
|
||||
|
||||
if (!datasetRes)
|
||||
return null
|
||||
|
||||
return (
|
||||
<DatasetDetailContext.Provider value={{
|
||||
indexingTechnique: datasetRes.indexing_technique,
|
||||
dataset: datasetRes,
|
||||
mutateDatasetRes,
|
||||
}}
|
||||
>
|
||||
<div className="flex min-h-0 flex-1 flex-col px-2 pb-2">
|
||||
<div className="py-2">
|
||||
<DatasetInfo expand />
|
||||
</div>
|
||||
<div className="px-2 py-2">
|
||||
<Divider
|
||||
type="horizontal"
|
||||
bgStyle="gradient"
|
||||
className="my-0 h-px bg-linear-to-r from-divider-subtle to-background-gradient-mask-transparent"
|
||||
/>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-y-0.5 px-1 py-2">
|
||||
{navigation.map(item => (
|
||||
<NavLink
|
||||
key={item.href}
|
||||
mode="expand"
|
||||
iconMap={{ selected: item.selectedIcon, normal: item.icon }}
|
||||
name={item.name}
|
||||
href={item.href}
|
||||
disabled={item.disabled}
|
||||
pathname={pathname}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
{!isCurrentWorkspaceDatasetOperator && (
|
||||
<ExtraInfo
|
||||
relatedApps={relatedApps}
|
||||
expand
|
||||
documentCount={datasetRes.document_count}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DatasetDetailContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetDetailSection
|
||||
54
web/app/components/app-sidebar/dataset-detail-top.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
|
||||
const DatasetDetailTop = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-3 pr-3 pl-1">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<div className="flex shrink-0 items-center py-1 pr-1 pl-0.5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.back', { ns: 'common' })}
|
||||
className="flex size-4 items-center justify-center text-text-tertiary hover:text-text-secondary"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<span aria-hidden className="i-ri-arrow-left-s-line size-4" />
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
aria-label={t('mainNav.home', { ns: 'common' })}
|
||||
className="flex size-4 items-center justify-center text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-main-nav-app-home size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<span className="mx-1.5 shrink-0 system-md-regular text-text-quaternary">
|
||||
/
|
||||
</span>
|
||||
<span className="shrink-0 truncate system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('menus.datasets', { ns: 'common' })}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('gotoAnything.searchTitle', { ns: 'app' })}
|
||||
className="flex shrink-0 items-center gap-1 overflow-hidden rounded-[10px] p-1 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={() => window.dispatchEvent(new Event(GOTO_ANYTHING_OPEN_EVENT))}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-main-nav-quick-search size-4" />
|
||||
<span className="rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
⌘K
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetDetailTop
|
||||
@ -206,6 +206,19 @@ describe('NavLink Animation and Layout Issues', () => {
|
||||
expect(linkElement).toHaveClass('bg-components-menu-item-bg-active')
|
||||
expect(linkElement).toHaveClass('text-text-accent-light-mode-only')
|
||||
})
|
||||
|
||||
it('should use pathname when rendered outside the app detail route segment', () => {
|
||||
render(
|
||||
<NavLink
|
||||
{...mockProps}
|
||||
href="/app/123/logs"
|
||||
pathname="/app/123/annotations"
|
||||
/>,
|
||||
)
|
||||
|
||||
const linkElement = screen.getByTestId('nav-link')
|
||||
expect(linkElement).toHaveClass('bg-components-menu-item-bg-active')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Text Animation Classes', () => {
|
||||
|
||||
@ -21,6 +21,7 @@ export type NavLinkProps = {
|
||||
}
|
||||
mode?: string
|
||||
disabled?: boolean
|
||||
pathname?: string
|
||||
}
|
||||
|
||||
const NavLink = ({
|
||||
@ -29,10 +30,11 @@ const NavLink = ({
|
||||
iconMap,
|
||||
mode = 'expand',
|
||||
disabled = false,
|
||||
pathname,
|
||||
}: NavLinkProps) => {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
const formattedSegment = (() => {
|
||||
let res = segment?.toLowerCase()
|
||||
let res = pathname ? pathname.toLowerCase().split('/').filter(Boolean).pop() : segment?.toLowerCase()
|
||||
// logs and annotations use the same nav
|
||||
if (res === 'annotations')
|
||||
res = 'logs'
|
||||
|
||||
@ -20,9 +20,9 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { DatasetPermission } from '@/models/datasets'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
@ -53,7 +53,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const docLink = useDocLink()
|
||||
const ref = useRef(null)
|
||||
const isExternal = currentDataset.provider === 'external'
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const [localeCurrentDataset, setLocaleCurrentDataset] = useState({ ...currentDataset })
|
||||
@ -284,7 +284,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="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"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
onClick={() => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
>
|
||||
{t('form.embeddingModelTipLink', { ns: 'datasetSettings' })}
|
||||
</button>
|
||||
|
||||
@ -40,9 +40,9 @@ import {
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel,
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import { ANNOTATION_DEFAULT, DATASET_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
@ -109,7 +109,7 @@ export type ConfigurationViewModel = {
|
||||
export const useConfiguration = (): ConfigurationViewModel => {
|
||||
const { t } = useTranslation()
|
||||
const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
|
||||
const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
@ -669,7 +669,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
onCloseSelectDataSet: hideSelectDataSet,
|
||||
onCompletionParamsChange: setCompletionParams,
|
||||
onConfirmUseGPT4: () => {
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
setShowUseGPT4Confirm(false)
|
||||
},
|
||||
onEnableMultipleModelDebug: handleDebugWithMultipleModelChange,
|
||||
@ -677,7 +677,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
onHideDebugPanel: hideDebugPanel,
|
||||
onModelChange: setModel,
|
||||
onMultipleModelConfigsChange: handleMultipleModelConfigsChange,
|
||||
onOpenAccountSettings: () => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER }),
|
||||
onOpenAccountSettings: () => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER }),
|
||||
onOpenDebugPanel: showDebugPanel,
|
||||
onSaveHistory: (data) => {
|
||||
setConversationHistoriesRole(data)
|
||||
|
||||
@ -12,6 +12,7 @@ const mockPush = vi.fn()
|
||||
const mockToastSuccess = vi.fn()
|
||||
const mockToastError = vi.fn()
|
||||
const mockTrackCreateApp = vi.fn()
|
||||
const mockInvalidateAppList = vi.hoisted(() => vi.fn())
|
||||
let latestDebounceFn = () => {}
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
@ -98,6 +99,9 @@ vi.mock('@/utils/create-app-tracking', () => ({
|
||||
vi.mock('@/service/apps', () => ({
|
||||
importDSL: (...args: unknown[]) => mockImportDSL(...args),
|
||||
}))
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppList: () => mockInvalidateAppList,
|
||||
}))
|
||||
vi.mock('@/service/explore', () => ({
|
||||
fetchAppDetail: (...args: unknown[]) => mockFetchAppDetail(...args),
|
||||
}))
|
||||
@ -253,6 +257,7 @@ describe('Apps', () => {
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('created-app-id')
|
||||
expect(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY)).toBe('1')
|
||||
expect(mockInvalidateAppList).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetRedirection).toHaveBeenCalledWith(true, {
|
||||
id: 'created-app-id',
|
||||
mode: AppModeEnum.CHAT,
|
||||
|
||||
@ -21,6 +21,7 @@ import { DSLImportMode } from '@/models/app'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { useInvalidateAppList } from '@/service/use-apps'
|
||||
import { useExploreAppList } from '@/service/use-explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
@ -45,6 +46,7 @@ const Apps = ({
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { push } = useRouter()
|
||||
const invalidateAppList = useInvalidateAppList()
|
||||
const allCategoriesEn = AppCategories.RECOMMENDED
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
@ -136,6 +138,7 @@ const Apps = ({
|
||||
if (app.app_id)
|
||||
await handleCheckPluginDependencies(app.app_id)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
invalidateAppList()
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push)
|
||||
}
|
||||
catch {
|
||||
|
||||
@ -15,6 +15,7 @@ import CreateAppModal from '../index'
|
||||
const ahooksMocks = vi.hoisted(() => ({
|
||||
keyPressHandlers: [] as Array<() => void>,
|
||||
}))
|
||||
const mockInvalidateAppList = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceFn: <T extends (...args: unknown[]) => unknown>(fn: T) => {
|
||||
@ -37,6 +38,9 @@ vi.mock('@/utils/create-app-tracking', () => ({
|
||||
vi.mock('@/service/apps', () => ({
|
||||
createApp: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppList: () => mockInvalidateAppList,
|
||||
}))
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
mockToastSuccess: vi.fn(),
|
||||
mockToastError: vi.fn(),
|
||||
@ -175,6 +179,7 @@ describe('CreateAppModal', () => {
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
await waitFor(() => expect(mockSetItem).toHaveBeenCalledWith(NEED_REFRESH_APP_LIST_KEY, '1'))
|
||||
expect(mockInvalidateAppList).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => expect(mockGetRedirection).toHaveBeenCalledWith(true, mockApp, mockPush))
|
||||
})
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { createApp } from '@/service/apps'
|
||||
import { useInvalidateAppList } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { trackCreateApp } from '@/utils/create-app-tracking'
|
||||
@ -54,6 +55,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const invalidateAppList = useInvalidateAppList()
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
|
||||
@ -85,13 +87,14 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
onSuccess()
|
||||
onClose()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
invalidateAppList()
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('newApp.appCreateFailed', { ns: 'app' }))
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [name, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor])
|
||||
}, [name, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor, invalidateAppList])
|
||||
|
||||
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
|
||||
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
|
||||
|
||||
@ -11,6 +11,7 @@ const mockImportDSLConfirm = vi.fn()
|
||||
const mockTrackCreateApp = vi.fn()
|
||||
const mockHandleCheckPluginDependencies = vi.fn()
|
||||
const mockGetRedirection = vi.fn()
|
||||
const mockInvalidateAppList = vi.hoisted(() => vi.fn())
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
call: vi.fn(),
|
||||
success: vi.fn(),
|
||||
@ -52,6 +53,9 @@ vi.mock('@/service/apps', () => ({
|
||||
importDSL: (...args: unknown[]) => mockImportDSL(...args),
|
||||
importDSLConfirm: (...args: unknown[]) => mockImportDSLConfirm(...args),
|
||||
}))
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppList: () => mockInvalidateAppList,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
|
||||
usePluginDependencies: () => ({
|
||||
@ -201,6 +205,7 @@ describe('CreateFromDSLModal', () => {
|
||||
expect(handleSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||
expect(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY)).toBe('1')
|
||||
expect(mockInvalidateAppList).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('app-1')
|
||||
expect(mockGetRedirection).toHaveBeenCalledWith(true, { id: 'app-1', mode: 'chat' }, mockPush)
|
||||
})
|
||||
@ -305,6 +310,7 @@ describe('CreateFromDSLModal', () => {
|
||||
import_id: 'import-3',
|
||||
})
|
||||
expect(mockTrackCreateApp).toHaveBeenCalledWith({ appMode: AppModeEnum.WORKFLOW })
|
||||
expect(mockInvalidateAppList).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should close the DSL mismatch modal when dialog requests close', async () => {
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
importDSL,
|
||||
importDSLConfirm,
|
||||
} from '@/service/apps'
|
||||
import { useInvalidateAppList } from '@/service/use-apps'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { trackCreateApp } from '@/utils/create-app-tracking'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
@ -74,6 +75,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||
const invalidateAppList = useInvalidateAppList()
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
|
||||
@ -124,6 +126,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
: undefined,
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
invalidateAppList()
|
||||
if (app_id)
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||
@ -181,6 +184,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
if (app_id)
|
||||
await handleCheckPluginDependencies(app_id)
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
invalidateAppList()
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { cleanup, screen } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
assertions,
|
||||
clearAllMocks,
|
||||
defaultModalContext,
|
||||
interactions,
|
||||
mockRouterPush,
|
||||
mockUseModalContext,
|
||||
scenarios,
|
||||
textKeys,
|
||||
@ -75,14 +75,13 @@ describe('APIKeyInfoPanel - Cloud Edition', () => {
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call setShowAccountSettingModal when set API button is clicked', () => {
|
||||
it('should navigate to the model provider page when set API button is clicked', () => {
|
||||
scenarios.withMockModal(mockSetShowAccountSettingModal)
|
||||
|
||||
interactions.clickMainButton()
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
})
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/integrations/model-provider')
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should hide panel when close button is clicked', () => {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { cleanup, screen } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
assertions,
|
||||
clearAllMocks,
|
||||
defaultModalContext,
|
||||
interactions,
|
||||
mockRouterPush,
|
||||
mockUseModalContext,
|
||||
scenarios,
|
||||
textKeys,
|
||||
@ -88,14 +88,13 @@ describe('APIKeyInfoPanel - Community Edition', () => {
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call setShowAccountSettingModal when set API button is clicked', () => {
|
||||
it('should navigate to the model provider page when set API button is clicked', () => {
|
||||
scenarios.withMockModal(mockSetShowAccountSettingModal)
|
||||
|
||||
interactions.clickMainButton()
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
})
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/integrations/model-provider')
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should hide panel when close button is clicked', () => {
|
||||
|
||||
@ -4,11 +4,15 @@ import type { ModalContextState } from '@/context/modal-context'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { useModalContext as actualUseModalContext } from '@/context/modal-context'
|
||||
import { useModalContext as actualUseModalContext, useModalContextSelector as actualUseModalContextSelector } from '@/context/modal-context'
|
||||
|
||||
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
|
||||
import APIKeyInfoPanel from './index'
|
||||
|
||||
const { mockRouterPush } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the modules before importing the functions
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
@ -16,11 +20,19 @@ vi.mock('@/context/provider-context', () => ({
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
useModalContextSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Type casting for mocks
|
||||
const mockUseProviderContext = actualUseProviderContext as MockedFunction<typeof actualUseProviderContext>
|
||||
const mockUseModalContext = actualUseModalContext as MockedFunction<typeof actualUseModalContext>
|
||||
const mockUseModalContextSelector = actualUseModalContextSelector as MockedFunction<typeof actualUseModalContextSelector>
|
||||
|
||||
// Default mock data
|
||||
const defaultProviderContext = {
|
||||
@ -94,6 +106,13 @@ function setupMocks(overrides: MockOverrides = {}) {
|
||||
...defaultModalContext,
|
||||
...overrides.modalContext,
|
||||
})
|
||||
|
||||
mockUseModalContextSelector.mockImplementation(selector =>
|
||||
selector({
|
||||
...defaultModalContext,
|
||||
...overrides.modalContext,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Custom render function
|
||||
@ -212,4 +231,4 @@ export function clearAllMocks() {
|
||||
}
|
||||
|
||||
// Export mock functions for external access
|
||||
export { defaultModalContext, mockUseModalContext }
|
||||
export { defaultModalContext, mockRouterPush, mockUseModalContext, mockUseModalContextSelector }
|
||||
|
||||
@ -8,15 +8,15 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
const APIKeyInfoPanel: FC = () => {
|
||||
const isCloud = !IS_CE_EDITION
|
||||
|
||||
const { isAPIKeySet } = useProviderContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -49,7 +49,7 @@ const APIKeyInfoPanel: FC = () => {
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-2 space-x-2"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
onClick={() => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
>
|
||||
<div className="text-sm font-medium">{t('apiKeyInfo.setAPIBtn', { ns: 'appOverview' })}</div>
|
||||
<LinkExternal02 className="h-4 w-4" />
|
||||
|
||||