Merge remote-tracking branch 'dev/main_form_dev'

pull/19011/head
安泽芃 1 year ago
commit dd19bdce67

@ -0,0 +1,91 @@
# Dify 后端 API
## 使用方法
> [重要事项]
>
> 在 v1.3.0 版本中,`poetry` 已被
> [ `uv` ](https://docs.astral.sh/uv/) 替代,作为 Dify API 后端服务的包管理器。
1. 启动 docker-compose 栈
后端需要一些中间件,包括 PostgreSQL、Redis 和 Weaviate可以使用 `docker-compose` 一起启动。
```bash
cd ../docker
cp middleware.env.example middleware.env
# 如果不使用 weaviate请将配置文件更改为其他向量数据库
docker compose -f docker-compose.middleware.yaml --profile weaviate -p dify up -d
cd ../api
```
2. 将 `.env.example` 复制为 `.env`
```cli
cp .env.example .env
```
3. 在 `.env` 文件中生成一个 `SECRET_KEY`
Linux 系统的 bash 命令
```bash for Linux
sed -i "/^SECRET_KEY=/c\SECRET_KEY=$(openssl rand -base64 42)" .env
```
Mac 系统的 bash 命令
```bash for Mac
secret_key=$(openssl rand -base64 42)
sed -i '' "/^SECRET_KEY=/c\\
SECRET_KEY=${secret_key}" .env
```
4. 创建环境。
Dify API 服务使用 [UV](https://docs.astral.sh/uv/) 来管理依赖项。
首先,如果还没有安装 uv 包管理器,需要先安装它。
```bash
pip install uv
# 或者在 macOS 上
brew install uv
```
5. 安装依赖项
```bash
uv sync --dev
```
6. 运行迁移
在首次启动之前,将数据库迁移到最新版本。
```bash
uv run flask db upgrade
```
7. 启动后端
```bash
uv run flask run --host 0.0.0.0 --port=5001 --debug
```
8. 启动 Dify [web](../web) 服务。
9. 通过访问 `http://localhost:3000` 来设置你的应用程序。
10. 如果你需要处理和调试异步任务(例如数据集导入和文档索引),请启动工作进程服务。
```bash
uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion
```
## 测试
1. 为后端和测试环境安装依赖项
```bash
uv sync --dev
```
2. 使用 `pyproject.toml` 文件中 `tool.pytest_env` 部分模拟的系统环境变量在本地运行测试
```bash
uv run -P api bash dev/pytest/pytest_all_tests.sh
```

@ -1,4 +1,5 @@
import uuid import uuid
import json
from typing import cast from typing import cast
from flask_login import current_user # type: ignore from flask_login import current_user # type: ignore
@ -139,6 +140,8 @@ class AppApi(Resource):
parser.add_argument("icon", type=str, location="json") parser.add_argument("icon", type=str, location="json")
parser.add_argument("icon_background", type=str, location="json") parser.add_argument("icon_background", type=str, location="json")
parser.add_argument("use_icon_as_answer_icon", type=bool, location="json") parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
parser.add_argument("permission", type=str, location="json")
parser.add_argument("partial_member_list", type=list, location="json")
args = parser.parse_args() args = parser.parse_args()
app_service = AppService() app_service = AppService()

@ -98,6 +98,8 @@ app_partial_fields = {
"updated_by": fields.String, "updated_by": fields.String,
"updated_at": TimestampField, "updated_at": TimestampField,
"tags": fields.List(fields.Nested(tag_fields)), "tags": fields.List(fields.Nested(tag_fields)),
"permission": fields.String,
"permission_account_ids": fields.List(fields.String),
} }

@ -101,6 +101,7 @@ class App(Base):
updated_by = db.Column(StringUUID, nullable=True) updated_by = db.Column(StringUUID, nullable=True)
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
permission = db.Column(db.String(255))
@property @property
def desc_or_prompt(self): def desc_or_prompt(self):
@ -295,6 +296,23 @@ class App(Base):
return tags or [] return tags or []
class AppPermission(Base):
__tablename__ = "app_permissions"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="app_permission_pkey"),
db.Index("idx_app_permissions_app_id", "app_id"),
db.Index("idx_app_permissions_account_id", "account_id"),
db.Index("idx_app_permissions_tenant_id", "tenant_id"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
app_id = db.Column(StringUUID, nullable=False)
account_id = db.Column(StringUUID, nullable=False)
has_permission = db.Column(db.Boolean, nullable=False, server_default=db.text("true"))
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
tenant_id = db.Column(StringUUID, nullable=False)
class AppModelConfig(Base): class AppModelConfig(Base):
__tablename__ = "app_model_configs" __tablename__ = "app_model_configs"
__table_args__ = (db.PrimaryKeyConstraint("id", name="app_model_config_pkey"), db.Index("app_app_id_idx", "app_id")) __table_args__ = (db.PrimaryKeyConstraint("id", name="app_model_config_pkey"), db.Index("app_app_id_idx", "app_id"))

@ -0,0 +1,191 @@
from sqlalchemy.orm import Session
from extensions.ext_database import db
from models.model import AppPermission
class AppPermissionService:
@classmethod
def get_app_permissions_by_app_id(cls, app_id: str):
"""
Get a list of account IDs that have permission for an app.
Args:
app_id (str): The ID of the app.
Returns:
list: A list of account IDs with permissions for the app.
"""
with Session(db.engine) as session:
permissions = session.query(AppPermission).filter(
AppPermission.app_id == app_id,
AppPermission.has_permission == True
).all()
return [permission.account_id for permission in permissions]
@classmethod
def get_app_permissions_by_account_id(cls, account_id: str):
"""
Get a list of app IDs that an account has permission for.
Args:
account_id (str): The ID of the account.
Returns:
list: A list of app IDs the account has permission for.
"""
with Session(db.engine) as session:
permissions = session.query(AppPermission).filter(
AppPermission.account_id == account_id,
AppPermission.has_permission == True
).all()
return [permission.app_id for permission in permissions]
@classmethod
def update_app_permissions(cls, tenant_id: str, app_id: str, account_ids: list):
"""
Update the permissions for an app by replacing all existing permissions.
Args:
tenant_id (str): The ID of the tenant.
app_id (str): The ID of the app.
account_ids (list): A list of account IDs to grant permission to.
Returns:
bool: True if the operation succeeds.
"""
try:
with Session(db.engine) as session:
# Delete existing permissions for the app
session.query(AppPermission).filter(
AppPermission.app_id == app_id
).delete()
# Create new permissions
permissions = []
for account_id in account_ids:
permission = AppPermission(
tenant_id=tenant_id,
app_id=app_id,
account_id=account_id,
has_permission=True
)
permissions.append(permission)
session.add_all(permissions)
session.commit()
return True
except Exception as e:
db.session.rollback()
raise e
@classmethod
def add_app_permission(cls, tenant_id: str, app_id: str, account_id: str):
"""
Add permission for an account to access an app.
Args:
tenant_id (str): The ID of the tenant.
app_id (str): The ID of the app.
account_id (str): The ID of the account to grant permission to.
Returns:
AppPermission: The created permission object.
"""
with Session(db.engine) as session:
# Check if permission already exists
existing_permission = session.query(AppPermission).filter(
AppPermission.app_id == app_id,
AppPermission.account_id == account_id
).first()
if existing_permission:
existing_permission.has_permission = True
session.commit()
return existing_permission
# Create new permission
permission = AppPermission(
tenant_id=tenant_id,
app_id=app_id,
account_id=account_id,
has_permission=True
)
session.add(permission)
session.commit()
return permission
@classmethod
def remove_app_permission(cls, app_id: str, account_id: str):
"""
Remove permission for an account to access an app.
Args:
app_id (str): The ID of the app.
account_id (str): The ID of the account to remove permission from.
Returns:
bool: True if the permission was removed.
"""
with Session(db.engine) as session:
permission = session.query(AppPermission).filter(
AppPermission.app_id == app_id,
AppPermission.account_id == account_id
).first()
if permission:
session.delete(permission)
session.commit()
return True
return False
@classmethod
def check_app_permission(cls, app_id: str, account_id: str):
"""
Check if an account has permission to access an app.
Args:
app_id (str): The ID of the app.
account_id (str): The ID of the account.
Returns:
bool: True if the account has permission.
"""
with Session(db.engine) as session:
permission = session.query(AppPermission).filter(
AppPermission.app_id == app_id,
AppPermission.account_id == account_id,
AppPermission.has_permission == True
).first()
return permission is not None
@classmethod
def clear_app_permissions(cls, app_id: str):
"""
Clear all permissions for an app.
Args:
app_id (str): The ID of the app.
Returns:
bool: True if the operation succeeds.
"""
try:
with Session(db.engine) as session:
session.query(AppPermission).filter(
AppPermission.app_id == app_id
).delete()
session.commit()
return True
except Exception as e:
db.session.rollback()
raise e

@ -18,8 +18,9 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager
from events.app_event import app_was_created from events.app_event import app_was_created
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Account from models.account import Account
from models.model import App, AppMode, AppModelConfig from models.model import App, AppMode, AppModelConfig, AppPermission
from models.tools import ApiToolProvider from models.tools import ApiToolProvider
from services.app_permission_service import AppPermissionService
from services.tag_service import TagService from services.tag_service import TagService
from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task
@ -59,6 +60,23 @@ class AppService:
filters.append(App.id.in_(target_ids)) filters.append(App.id.in_(target_ids))
else: else:
return None return None
# Add permission-based filtering
from sqlalchemy import or_
permission_filters = [
# All team members can see apps with permission "all_team_members" or null
or_(App.permission == "all_team_members", App.permission == None),
# Creator can see their own apps
App.created_by == user_id,
# Users with explicit permission in app_permissions table
App.id.in_(
db.session.query(AppPermission.app_id)
.filter(
AppPermission.account_id == user_id,
AppPermission.has_permission == True
)
)
]
filters.append(or_(*permission_filters))
app_models = db.paginate( app_models = db.paginate(
db.select(App).where(*filters).order_by(App.created_at.desc()), db.select(App).where(*filters).order_by(App.created_at.desc()),
@ -66,7 +84,8 @@ class AppService:
per_page=args["limit"], per_page=args["limit"],
error_out=False, error_out=False,
) )
for app in app_models.items:
app.permission_account_ids = AppPermissionService.get_app_permissions_by_app_id(app.id)
return app_models return app_models
def create_app(self, tenant_id: str, args: dict, account: Account) -> App: def create_app(self, tenant_id: str, args: dict, account: Account) -> App:
@ -231,7 +250,16 @@ class AppService:
app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False) app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False)
app.updated_by = current_user.id app.updated_by = current_user.id
app.updated_at = datetime.now(UTC).replace(tzinfo=None) app.updated_at = datetime.now(UTC).replace(tzinfo=None)
app.permission = args.get("permission")
db.session.commit() db.session.commit()
tenant_id = current_user.current_tenant_id
# Handle app permissions
if args.get("permission") == "partial_members" and args.get("partial_member_list"):
account_ids = args.get("partial_member_list")
AppPermissionService.update_app_permissions(tenant_id, app.id, account_ids)
# Clear permissions if permission is not partial_members
elif args.get("permission") != "partial_members":
AppPermissionService.clear_app_permissions(app.id)
return app return app
@ -304,6 +332,9 @@ class AppService:
Delete app Delete app
:param app: App instance :param app: App instance
""" """
# Clear app permissions
AppPermissionService.clear_app_permissions(app.id)
db.session.delete(app) db.session.delete(app)
db.session.commit() db.session.commit()
@ -373,3 +404,47 @@ class AppService:
meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"} meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"}
return meta return meta
def get_app_permission_member_list(self, app_id: str) -> list:
"""
Get the list of account IDs that have permission to access the app
Args:
app_id (str): The ID of the app
Returns:
list: List of account IDs with permission to access the app
"""
return AppPermissionService.get_app_permissions_by_app_id(app_id)
def check_app_permission(self, app: App, user: Account) -> bool:
"""
Check if a user has permission to access an app
Args:
app (App): The app to check permission for
user (Account): The user account to check
Returns:
bool: True if the user has permission, False otherwise
"""
# App owner always has permission
if app.created_by == user.id:
return True
# Tenant owner/admin always has permission
if user.is_owner or user.is_admin:
return True
# Check app permission setting
if not app.permission or app.permission == "all_team_members":
# All team members have permission
return True
elif app.permission == "only_me":
# Only the app creator has permission
return app.created_by == user.id
elif app.permission == "partial_members":
# Check if the user is in the partial member list
return AppPermissionService.check_app_permission(app.id, user.id)
return False

@ -81,6 +81,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
icon_background, icon_background,
description, description,
use_icon_as_answer_icon, use_icon_as_answer_icon,
permission,
partial_member_list
}) => { }) => {
try { try {
await updateAppInfo({ await updateAppInfo({
@ -91,6 +93,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
icon_background, icon_background,
description, description,
use_icon_as_answer_icon, use_icon_as_answer_icon,
permission,
partial_member_list
}) })
setShowEditModal(false) setShowEditModal(false)
notify({ notify({
@ -381,6 +385,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
show={showEditModal} show={showEditModal}
onConfirm={onEdit} onConfirm={onEdit}
onHide={() => setShowEditModal(false)} onHide={() => setShowEditModal(false)}
appPermission={app.permission}
appSelectedMemberIDs={app.permission_account_ids}
/> />
)} )}
{showDuplicateModal && ( {showDuplicateModal && (

@ -1,8 +1,12 @@
'use client' 'use client'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine, RiArrowDownSLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks' import { useDebounceFn, useKeyPress } from 'ahooks'
import { RiCloseLine as RemixIconCloseLine } from '@remixicon/react'
import cn from 'classnames'
import { Users01, UsersPlus } from '@/app/components/base/icons/src/vender/solid/users'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import AppIconPicker from '../../base/app-icon-picker' import AppIconPicker from '../../base/app-icon-picker'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
@ -11,10 +15,15 @@ import Textarea from '@/app/components/base/textarea'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Avatar from '@/app/components/base/avatar'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog' import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import type { AppIconType } from '@/types/app' import type { AppIconType } from '@/types/app'
import { fetchMembers } from '@/service/common'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import type { Member } from '@/models/common'
export type CreateAppModalProps = { export type CreateAppModalProps = {
show: boolean show: boolean
@ -27,6 +36,8 @@ export type CreateAppModalProps = {
appIconUrl?: string | null appIconUrl?: string | null
appMode?: string appMode?: string
appUseIconAsAnswerIcon?: boolean appUseIconAsAnswerIcon?: boolean
appPermission?: string
appSelectedMemberIDs?: string[]
onConfirm: (info: { onConfirm: (info: {
name: string name: string
icon_type: AppIconType icon_type: AppIconType
@ -34,6 +45,8 @@ export type CreateAppModalProps = {
icon_background?: string icon_background?: string
description: string description: string
use_icon_as_answer_icon?: boolean use_icon_as_answer_icon?: boolean
permission: 'only_me' | 'all_team_members' | 'partial_members'
partial_member_list?: string[]
}) => Promise<void> }) => Promise<void>
confirmDisabled?: boolean confirmDisabled?: boolean
onHide: () => void onHide: () => void
@ -50,6 +63,8 @@ const CreateAppModal = ({
appDescription, appDescription,
appMode, appMode,
appUseIconAsAnswerIcon, appUseIconAsAnswerIcon,
appPermission = 'only_me',
appSelectedMemberIDs,
onConfirm, onConfirm,
confirmDisabled, confirmDisabled,
onHide, onHide,
@ -65,7 +80,15 @@ const CreateAppModal = ({
const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [description, setDescription] = useState(appDescription || '') const [description, setDescription] = useState(appDescription || '')
const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false) const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false)
const [permission, setPermission] = useState<'only_me' | 'all_team_members' | 'partial_members'>(appPermission)
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(appSelectedMemberIDs || [])
const [isPermissionSelectorOpen, setIsPermissionSelectorOpen] = useState(false)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const [memberList, setMemberList] = useState<Member[]>([])
const { userProfile, currentWorkspace } = useAppContext()
const { plan, enableBilling } = useProviderContext() const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
@ -81,9 +104,56 @@ const CreateAppModal = ({
icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined, icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined,
description, description,
use_icon_as_answer_icon: useIconAsAnswerIcon, use_icon_as_answer_icon: useIconAsAnswerIcon,
permission,
partial_member_list: permission === 'partial_members' ? selectedMemberIDs : [],
}) })
onHide() onHide()
}, [name, appIcon, description, useIconAsAnswerIcon, onConfirm, onHide, t]) }, [name, appIcon, description, useIconAsAnswerIcon, permission, selectedMemberIDs, onConfirm, onHide, t])
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const selectMember = (member: Member) => {
if (selectedMemberIDs.includes(member.id))
setSelectedMemberIDs(selectedMemberIDs.filter(v => v !== member.id))
else
setSelectedMemberIDs([...selectedMemberIDs, member.id])
}
const selectedMembersDisplay = React.useMemo(() => {
return [
userProfile,
...memberList.filter(member => member.id !== userProfile.id).filter(member => selectedMemberIDs.includes(member.id)),
].map(member => member.name).join(', ')
}, [userProfile, selectedMemberIDs, memberList])
const showMe = React.useMemo(() => {
return userProfile.name.includes(searchKeywords) || userProfile.email.includes(searchKeywords)
}, [searchKeywords, userProfile])
const filteredMemberList = React.useMemo(() => {
return memberList.filter(member => (member.name.includes(searchKeywords) || member.email.includes(searchKeywords)) && member.id !== userProfile.id)
}, [memberList, searchKeywords, userProfile])
useEffect(() => {
if (isPermissionSelectorOpen && permission === 'partial_members') {
(async () => {
try {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
setMemberList(accounts || [])
} catch (e) {
console.error('Failed to fetch members', e)
setMemberList([])
}
})()
}
}, [isPermissionSelectorOpen, permission])
const { run: handleSubmit } = useDebounceFn(submit, { wait: 300 }) const { run: handleSubmit } = useDebounceFn(submit, { wait: 300 })
@ -158,6 +228,119 @@ const CreateAppModal = ({
<p className='body-xs-regular text-text-tertiary'>{t('app.answerIcon.descriptionInExplore')}</p> <p className='body-xs-regular text-text-tertiary'>{t('app.answerIcon.descriptionInExplore')}</p>
</div> </div>
)} )}
{/* permissions */}
{isEditModal && (
<div className='pt-2'>
<div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>{t('app.permissions')}</div>
<PortalToFollowElem
open={isPermissionSelectorOpen}
onOpenChange={setIsPermissionSelectorOpen}
placement='bottom-start'
offset={4}
>
<PortalToFollowElemTrigger
onClick={() => setIsPermissionSelectorOpen(v => !v)}
className='block'
>
<div className={cn('flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-[6px] hover:bg-state-base-hover-alt',
isPermissionSelectorOpen && 'bg-state-base-hover-alt',
)}>
{permission === 'only_me' && (
<>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='mr-2 shrink-0' size={24} />
<div className='mr-2 grow text-sm leading-5 text-components-input-text-filled'>{t('app.permissionsOnlyMe')}</div>
</>
)}
{permission === 'all_team_members' && (
<>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
</div>
<div className='mr-2 grow text-sm leading-5 text-components-input-text-filled'>{t('app.permissionsAllMember')}</div>
</>
)}
{permission === 'partial_members' && (
<>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
</div>
<div title={selectedMembersDisplay} className='mr-2 grow truncate text-sm leading-5 text-components-input-text-filled'>{selectedMembersDisplay}</div>
</>
)}
<RiArrowDownSLine className={cn('h-4 w-4 shrink-0 text-text-secondary')} />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='w-[480px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-1'>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { setPermission('only_me'); setIsPermissionSelectorOpen(false); }}>
<div className='flex items-center gap-2'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='mr-2 shrink-0' size={24} />
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('app.permissionsOnlyMe')}</div>
{permission === 'only_me' && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { setPermission('all_team_members'); setIsPermissionSelectorOpen(false); }}>
<div className='flex items-center gap-2'>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
</div>
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('app.permissionsAllMember')}</div>
{permission === 'all_team_members' && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { setPermission('partial_members'); setSelectedMemberIDs([userProfile.id]); }}>
<div className='flex items-center gap-2'>
<div className={cn('mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#FFF6ED]', permission === 'partial_members' && '!bg-[#EEF4FF]')}>
<UsersPlus className={cn('h-3.5 w-3.5 text-[#FB6514]', permission === 'partial_members' && '!text-[#444CE7]')} />
</div>
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('app.permissionsInvitedMembers')}</div>
{permission === 'partial_members' && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
</div>
{permission === 'partial_members' && (
<div className='max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular pb-1 pl-1 pr-1'>
<div className='sticky left-0 top-0 z-10 bg-white p-2 pb-1'>
<Input
showLeftIcon
showClearIcon
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
placeholder={t('common.operation.search') || ''}
/>
</div>
{showMe && (
<div className='flex items-center gap-2 rounded-lg py-1 pl-3 pr-[10px]'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0' size={24} />
<div className='grow'>
<div className='truncate text-[13px] font-medium leading-[18px] text-text-secondary'>
{userProfile.name}
<span className='text-xs font-normal text-text-tertiary'>{` (${t('datasetSettings.form.me')})`}</span>
</div>
<div className='truncate text-xs leading-[18px] text-text-tertiary'>{userProfile.email}</div>
</div>
<Check className='h-4 w-4 shrink-0 text-text-accent opacity-30' />
</div>
)}
{filteredMemberList.map(member => (
<div key={member.id} className='flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-3 pr-[10px] hover:bg-state-base-hover' onClick={() => selectMember(member)}>
<Avatar avatar={member.avatar_url} name={member.name} className='shrink-0' size={24} />
<div className='grow'>
<div className='truncate text-[13px] font-medium leading-[18px] text-text-secondary'>{member.name}</div>
<div className='truncate text-xs leading-[18px] text-text-tertiary'>{member.email}</div>
</div>
{selectedMemberIDs.includes(member.id) && <Check className='h-4 w-4 shrink-0 text-text-accent' />}
</div>
))}
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)}
{!isEditModal && isAppsFull && <AppsFull className='mt-4' loc='app-explore-create' />} {!isEditModal && isAppsFull && <AppsFull className='mt-4' loc='app-explore-create' />}
</div> </div>
<div className='flex flex-row-reverse'> <div className='flex flex-row-reverse'>

@ -167,6 +167,10 @@ const translation = {
title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖', title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖',
description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll', description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll',
}, },
permissions: 'Sichtbarkeit',
permissionsOnlyMe: 'Nur ich',
permissionsAllMember: 'Alle Teammitglieder',
permissionsInvitedMembers: 'Teilnehmer',
importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen', importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen',
duplicate: 'Duplikat', duplicate: 'Duplikat',
importFromDSL: 'Import von DSL', importFromDSL: 'Import von DSL',

@ -116,6 +116,10 @@ const translation = {
description: 'Whether to use the WebApp icon to replace 🤖 in the shared application', description: 'Whether to use the WebApp icon to replace 🤖 in the shared application',
descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore', descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore',
}, },
permissions: 'Visibility',
permissionsOnlyMe: 'Only me',
permissionsAllMember: 'All Team Members',
permissionsInvitedMembers: 'Invited Members',
switch: 'Switch to Workflow Orchestrate', switch: 'Switch to Workflow Orchestrate',
switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ',
switchTip: 'not allow', switchTip: 'not allow',

@ -165,6 +165,10 @@ const translation = {
descriptionInExplore: 'Si se debe usar el icono de la aplicación web para reemplazarlo 🤖 en Explore', descriptionInExplore: 'Si se debe usar el icono de la aplicación web para reemplazarlo 🤖 en Explore',
description: 'Si se va a usar el icono de la aplicación web para reemplazarlo 🤖 en la aplicación compartida', description: 'Si se va a usar el icono de la aplicación web para reemplazarlo 🤖 en la aplicación compartida',
}, },
permissions: 'Visibilidad',
permissionsOnlyMe: 'Solo yo',
permissionsAllMember: 'Todos los miembros del equipo',
permissionsInvitedMembers: 'Miembros invitados',
importFromDSLUrl: 'URL de origen', importFromDSLUrl: 'URL de origen',
importFromDSLUrlPlaceholder: 'Pegar enlace DSL aquí', importFromDSLUrlPlaceholder: 'Pegar enlace DSL aquí',
importFromDSL: 'Importar desde DSL', importFromDSL: 'Importar desde DSL',

@ -169,6 +169,10 @@ const translation = {
description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر', description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر',
title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید', title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید',
}, },
permissions: 'قابلیت دسترسی',
permissionsOnlyMe: 'فقط من',
permissionsAllMember: 'همه اعضای تیم',
permissionsInvitedMembers: 'اعضای دعوت شده',
mermaid: { mermaid: {
handDrawn: 'دست کشیده شده', handDrawn: 'دست کشیده شده',
classic: 'کلاسیک', classic: 'کلاسیک',

@ -165,6 +165,10 @@ const translation = {
title: 'Utiliser licône WebApp pour remplacer 🤖', title: 'Utiliser licône WebApp pour remplacer 🤖',
descriptionInExplore: 'Utilisation de licône WebApp pour remplacer 🤖 dans Explore', descriptionInExplore: 'Utilisation de licône WebApp pour remplacer 🤖 dans Explore',
}, },
permissions: 'Visibilité',
permissionsOnlyMe: 'Seul moi',
permissionsAllMember: 'Tous les membres du team',
permissionsInvitedMembers: 'Membres invités',
importFromDSLUrlPlaceholder: 'Collez le lien DSL ici', importFromDSLUrlPlaceholder: 'Collez le lien DSL ici',
importFromDSL: 'Importation à partir dune DSL', importFromDSL: 'Importation à partir dune DSL',
importFromDSLUrl: 'À partir de lURL', importFromDSLUrl: 'À partir de lURL',

@ -165,6 +165,10 @@ const translation = {
descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं', descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं',
description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं', description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं',
}, },
permissions: 'दृश्यता',
permissionsOnlyMe: 'केवल मैं',
permissionsAllMember: 'सभी टीम सदस्य',
permissionsInvitedMembers: 'आमंत्रित सदस्य',
importFromDSLFile: 'डीएसएल फ़ाइल से', importFromDSLFile: 'डीएसएल फ़ाइल से',
importFromDSLUrl: 'यूआरएल से', importFromDSLUrl: 'यूआरएल से',
importFromDSL: 'DSL से आयात करें', importFromDSL: 'DSL से आयात करें',

@ -177,6 +177,10 @@ const translation = {
title: 'Usa l\'icona WebApp per sostituire 🤖', title: 'Usa l\'icona WebApp per sostituire 🤖',
descriptionInExplore: 'Se utilizzare l\'icona WebApp per sostituirla 🤖 in Esplora', descriptionInExplore: 'Se utilizzare l\'icona WebApp per sostituirla 🤖 in Esplora',
}, },
permissions: 'Visibilità',
permissionsOnlyMe: 'Solo io',
permissionsAllMember: 'Tutti i membri del team',
permissionsInvitedMembers: 'Membri invitati',
importFromDSLUrl: 'Dall\'URL', importFromDSLUrl: 'Dall\'URL',
importFromDSLFile: 'Da file DSL', importFromDSLFile: 'Da file DSL',
importFromDSL: 'Importazione da DSL', importFromDSL: 'Importazione da DSL',

@ -170,6 +170,10 @@ const translation = {
description: '共有アプリケーションの中で Webアプリアイコンを使用して🤖を置き換えるかどうか', description: '共有アプリケーションの中で Webアプリアイコンを使用して🤖を置き換えるかどうか',
descriptionInExplore: 'ExploreでWebアプリアイコンを使用して🤖を置き換えるかどうか', descriptionInExplore: 'ExploreでWebアプリアイコンを使用して🤖を置き換えるかどうか',
}, },
permissions: '可視性',
permissionsOnlyMe: '私だけ',
permissionsAllMember: 'チームのすべてのメンバー',
permissionsInvitedMembers: '招待されたメンバー',
mermaid: { mermaid: {
handDrawn: '手描き', handDrawn: '手描き',
classic: 'クラシック', classic: 'クラシック',

@ -161,6 +161,10 @@ const translation = {
title: 'WebApp 아이콘을 사용하여 🤖', title: 'WebApp 아이콘을 사용하여 🤖',
descriptionInExplore: 'Explore에서 WebApp 아이콘을 사용하여 바꿀🤖지 여부', descriptionInExplore: 'Explore에서 WebApp 아이콘을 사용하여 바꿀🤖지 여부',
}, },
permissions: '가시성',
permissionsOnlyMe: '나만',
permissionsAllMember: '모든 팀 멤버',
permissionsInvitedMembers: '초대된 멤버',
importFromDSL: 'DSL에서 가져오기', importFromDSL: 'DSL에서 가져오기',
importFromDSLFile: 'DSL 파일에서', importFromDSLFile: 'DSL 파일에서',
importFromDSLUrl: 'URL에서', importFromDSLUrl: 'URL에서',

@ -172,6 +172,10 @@ const translation = {
title: 'Użyj ikony WebApp, aby zastąpić 🤖', title: 'Użyj ikony WebApp, aby zastąpić 🤖',
descriptionInExplore: 'Czy używać ikony aplikacji internetowej do zastępowania 🤖 w Eksploruj', descriptionInExplore: 'Czy używać ikony aplikacji internetowej do zastępowania 🤖 w Eksploruj',
}, },
permissions: 'Widoczność',
permissionsOnlyMe: 'Tylko ja',
permissionsAllMember: 'Wszystkie członkowie zespołu',
permissionsInvitedMembers: 'Zaproszone członkowie',
importFromDSL: 'Importowanie z DSL', importFromDSL: 'Importowanie z DSL',
importFromDSLUrl: 'Z adresu URL', importFromDSLUrl: 'Z adresu URL',
importFromDSLFile: 'Z pliku DSL', importFromDSLFile: 'Z pliku DSL',

@ -165,6 +165,10 @@ const translation = {
description: 'Se o ícone WebApp deve ser usado para substituir 🤖 no aplicativo compartilhado', description: 'Se o ícone WebApp deve ser usado para substituir 🤖 no aplicativo compartilhado',
title: 'Use o ícone do WebApp para substituir 🤖', title: 'Use o ícone do WebApp para substituir 🤖',
}, },
permissions: 'Visibilidade',
permissionsOnlyMe: 'Apenas eu',
permissionsAllMember: 'Todos os membros do time',
permissionsInvitedMembers: 'Membros convidados',
importFromDSLUrlPlaceholder: 'Cole o link DSL aqui', importFromDSLUrlPlaceholder: 'Cole o link DSL aqui',
importFromDSLUrl: 'Do URL', importFromDSLUrl: 'Do URL',
importFromDSLFile: 'Do arquivo DSL', importFromDSLFile: 'Do arquivo DSL',

@ -165,6 +165,10 @@ const translation = {
description: 'Dacă se utilizează pictograma WebApp pentru a înlocui 🤖 în aplicația partajată', description: 'Dacă se utilizează pictograma WebApp pentru a înlocui 🤖 în aplicația partajată',
title: 'Utilizați pictograma WebApp pentru a înlocui 🤖', title: 'Utilizați pictograma WebApp pentru a înlocui 🤖',
}, },
permissions: 'Vizibilitate',
permissionsOnlyMe: 'Doar mine',
permissionsAllMember: 'Toți membrii echipei',
permissionsInvitedMembers: 'Membri invitați',
importFromDSL: 'Import din DSL', importFromDSL: 'Import din DSL',
importFromDSLUrl: 'De la URL', importFromDSLUrl: 'De la URL',
importFromDSLUrlPlaceholder: 'Lipiți linkul DSL aici', importFromDSLUrlPlaceholder: 'Lipiți linkul DSL aici',

@ -169,6 +169,10 @@ const translation = {
description: 'Следует ли использовать значок WebApp для замены 🤖 в общем приложении', description: 'Следует ли использовать значок WebApp для замены 🤖 в общем приложении',
descriptionInExplore: 'Следует ли использовать значок WebApp для замены 🤖 в разделе "Обзор"', descriptionInExplore: 'Следует ли использовать значок WebApp для замены 🤖 в разделе "Обзор"',
}, },
permissions: 'Видимость',
permissionsOnlyMe: 'Только я',
permissionsAllMember: 'Все члены команды',
permissionsInvitedMembers: 'Приглашенные члены',
mermaid: { mermaid: {
handDrawn: 'Рисованный', handDrawn: 'Рисованный',
classic: 'Классический', classic: 'Классический',

@ -113,6 +113,10 @@ const translation = {
description: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v deljeni aplikaciji', description: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v deljeni aplikaciji',
descriptionInExplore: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v razdelku Razišči', descriptionInExplore: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v razdelku Razišči',
}, },
permissions: 'Vidnost',
permissionsOnlyMe: 'Samo jaz',
permissionsAllMember: 'Vsi člani skupine',
permissionsInvitedMembers: 'Pozvani člani',
switch: 'Preklopi na Workflow Orchestrate', switch: 'Preklopi na Workflow Orchestrate',
switchTipStart: 'Za vas bo ustvarjena nova kopija aplikacije, ki bo preklopila na Workflow Orchestrate. Nova kopija ne bo ', switchTipStart: 'Za vas bo ustvarjena nova kopija aplikacije, ki bo preklopila na Workflow Orchestrate. Nova kopija ne bo ',
switchTip: 'dovolila', switchTip: 'dovolila',

@ -109,6 +109,10 @@ const translation = {
description: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่', description: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่',
descriptionInExplore: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ใน Explore หรือไม่', descriptionInExplore: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ใน Explore หรือไม่',
}, },
permissions: 'ความสามารถเข้าถึง',
permissionsOnlyMe: 'ฉันเท่านั้น',
permissionsAllMember: 'ทุกสมาชิกของทีม',
permissionsInvitedMembers: 'สมาชิกที่เชิญ',
switch: 'เปลี่ยนไปใช้ Workflow Orchestrate', switch: 'เปลี่ยนไปใช้ Workflow Orchestrate',
switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration', switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration',
switchTip: 'ไม่อนุญาต', switchTip: 'ไม่อนุญาต',

@ -165,6 +165,10 @@ const translation = {
title: 'Değiştirmek 🤖 için WebApp simgesini kullanın', title: 'Değiştirmek 🤖 için WebApp simgesini kullanın',
description: 'Paylaşılan uygulamada değiştirmek 🤖 için WebApp simgesinin kullanılıp kullanılmayacağı', description: 'Paylaşılan uygulamada değiştirmek 🤖 için WebApp simgesinin kullanılıp kullanılmayacağı',
}, },
permissions: 'Görünürlük',
permissionsOnlyMe: 'Sadece ben',
permissionsAllMember: 'Tüm Takım Üyeleri',
permissionsInvitedMembers: 'Davetli Üyeler',
mermaid: { mermaid: {
handDrawn: 'Elle çizilmiş', handDrawn: 'Elle çizilmiş',
classic: 'Klasik', classic: 'Klasik',

@ -165,6 +165,10 @@ const translation = {
description: 'Чи слід використовувати піктограму WebApp для заміни 🤖 у спільній програмі', description: 'Чи слід використовувати піктограму WebApp для заміни 🤖 у спільній програмі',
descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді', descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді',
}, },
permissions: 'Відкритість',
permissionsOnlyMe: 'Тільки я',
permissionsAllMember: 'Всі члени команди',
permissionsInvitedMembers: 'Призначені члени',
importFromDSLUrl: 'З URL', importFromDSLUrl: 'З URL',
importFromDSL: 'Імпорт з DSL', importFromDSL: 'Імпорт з DSL',
importFromDSLUrlPlaceholder: 'Вставте посилання на DSL тут', importFromDSLUrlPlaceholder: 'Вставте посилання на DSL тут',

@ -165,6 +165,10 @@ const translation = {
descriptionInExplore: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong Khám phá hay không', descriptionInExplore: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong Khám phá hay không',
title: 'Sử dụng biểu tượng WebApp để thay thế 🤖', title: 'Sử dụng biểu tượng WebApp để thay thế 🤖',
}, },
permissions: 'Tính năng truy cập',
permissionsOnlyMe: 'Chỉ tôi',
permissionsAllMember: 'Tất cả thành viên nhóm',
permissionsInvitedMembers: 'Thành viên được mời',
importFromDSLFile: 'Từ tệp DSL', importFromDSLFile: 'Từ tệp DSL',
importFromDSL: 'Nhập từ DSL', importFromDSL: 'Nhập từ DSL',
importFromDSLUrlPlaceholder: 'Dán liên kết DSL vào đây', importFromDSLUrlPlaceholder: 'Dán liên kết DSL vào đây',

@ -117,6 +117,10 @@ const translation = {
description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖', description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖',
descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖', descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖',
}, },
permissions: '可见权限',
permissionsOnlyMe: '只有我',
permissionsAllMember: '所有团队成员',
permissionsInvitedMembers: '部分团队成员',
switch: '迁移为工作流编排', switch: '迁移为工作流编排',
switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将',
switchTip: '不能够', switchTip: '不能够',

@ -164,6 +164,10 @@ const translation = {
title: '使用 WebApp 圖示取代 🤖', title: '使用 WebApp 圖示取代 🤖',
description: '是否在共享應用程式中使用 WebApp 圖示進行取代 🤖', description: '是否在共享應用程式中使用 WebApp 圖示進行取代 🤖',
}, },
permissions: '可見權限',
permissionsOnlyMe: '只有我',
permissionsAllMember: '所有團隊成員',
permissionsInvitedMembers: '部分團隊成員',
importFromDSLUrl: '寄件者 URL', importFromDSLUrl: '寄件者 URL',
importFromDSL: '從 DSL 導入', importFromDSL: '從 DSL 導入',
importFromDSLFile: '從 DSL 檔', importFromDSLFile: '從 DSL 檔',

@ -28,8 +28,8 @@ export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: A
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } }) return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
} }
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon }) => { export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean; permission?: string; partial_member_list?: string[] }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, permission, partial_member_list }) => {
return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon } }) return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, permission, partial_member_list } })
} }
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppMode; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => { export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppMode; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {

@ -359,6 +359,10 @@ export type App = {
updated_at: number updated_at: number
updated_by?: string updated_by?: string
} }
/** Permission */
permission: string
/** Permission Account IDs */
permission_account_ids: string[]
} }
export type AppSSO = { export type AppSSO = {

Loading…
Cancel
Save