feat: Add the setting for app's visibility permission

pull/19011/head^2
安泽芃 1 year ago
parent 5471f125ee
commit ba965ecbda

@ -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

@ -20,6 +20,7 @@ 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
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
@ -66,7 +67,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 +233,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 +315,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 +387,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>
onHide: () => void onHide: () => void
} }
@ -49,6 +62,8 @@ const CreateAppModal = ({
appDescription, appDescription,
appMode, appMode,
appUseIconAsAnswerIcon, appUseIconAsAnswerIcon,
appPermission = 'only_me',
appSelectedMemberIDs,
onConfirm, onConfirm,
onHide, onHide,
}: CreateAppModalProps) => { }: CreateAppModalProps) => {
@ -63,7 +78,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)
@ -79,9 +102,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 })
@ -156,6 +226,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'>
@ -190,4 +373,4 @@ const CreateAppModal = ({
) )
} }
export default CreateAppModal export default CreateAppModal

@ -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: '不能够',

@ -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