瀏覽代碼

feat: new editor user permission profile (#4435)

Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: crazywoola <427733928@qq.com>
tags/0.6.11
Charles Zhou 1 年之前
父節點
當前提交
8bcc5a36bb
No account linked to committer's email address
共有 49 個文件被更改,包括 246 次插入126 次删除
  1. 13
    7
      api/controllers/console/app/app.py
  2. 13
    1
      api/controllers/console/app/conversation.py
  3. 2
    2
      api/controllers/console/app/site.py
  4. 6
    6
      api/controllers/console/datasets/datasets.py
  5. 10
    10
      api/controllers/console/datasets/datasets_document.py
  6. 4
    4
      api/controllers/console/datasets/datasets_segments.py
  7. 10
    10
      api/controllers/console/tag/tags.py
  8. 2
    2
      api/controllers/console/workspace/members.py
  9. 5
    3
      api/controllers/console/workspace/models.py
  10. 17
    1
      api/models/account.py
  11. 2
    0
      api/tests/unit_tests/models/test_account.py
  12. 16
    13
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx
  13. 4
    4
      web/app/(commonLayout)/apps/AppCard.tsx
  14. 2
    2
      web/app/(commonLayout)/apps/Apps.tsx
  15. 2
    2
      web/app/(commonLayout)/datasets/Datasets.tsx
  16. 3
    3
      web/app/components/app/create-app-modal/index.tsx
  17. 2
    2
      web/app/components/app/create-from-dsl-modal/index.tsx
  18. 5
    4
      web/app/components/app/overview/appCard.tsx
  19. 2
    2
      web/app/components/app/switch-app-modal/index.tsx
  20. 1
    1
      web/app/components/develop/secret-key/secret-key-modal.tsx
  21. 2
    2
      web/app/components/explore/app-list/index.tsx
  22. 1
    0
      web/app/components/header/account-setting/members-page/index.tsx
  23. 6
    2
      web/app/components/header/account-setting/members-page/invite-modal/index.tsx
  24. 2
    1
      web/app/components/header/account-setting/members-page/operation/index.tsx
  25. 4
    1
      web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx
  26. 3
    0
      web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx
  27. 5
    5
      web/app/components/header/app-nav/index.tsx
  28. 3
    3
      web/app/components/header/app-selector/index.tsx
  29. 3
    3
      web/app/components/header/index.tsx
  30. 2
    2
      web/app/components/header/nav/nav-selector/index.tsx
  31. 18
    14
      web/app/components/tools/provider/custom-create-card.tsx
  32. 4
    0
      web/app/components/tools/provider/detail.tsx
  33. 2
    0
      web/app/components/tools/setting/build-in/config-credentials.tsx
  34. 23
    10
      web/app/components/tools/workflow-tool/configure-button.tsx
  35. 4
    0
      web/context/app-context.tsx
  36. 3
    0
      web/i18n/de-DE/common.ts
  37. 6
    0
      web/i18n/en-US/common.ts
  38. 3
    0
      web/i18n/fr-FR/common.ts
  39. 3
    0
      web/i18n/ja-JP/common.ts
  40. 3
    0
      web/i18n/ko-KR/common.ts
  41. 3
    0
      web/i18n/pl-PL/common.ts
  42. 3
    0
      web/i18n/pt-BR/common.ts
  43. 3
    0
      web/i18n/ro-RO/common.ts
  44. 3
    0
      web/i18n/uk-UA/common.ts
  45. 3
    0
      web/i18n/vi-VN/common.ts
  46. 3
    0
      web/i18n/zh-Hans/common.ts
  47. 3
    0
      web/i18n/zh-Hant/common.ts
  48. 2
    2
      web/models/common.ts
  49. 2
    2
      web/utils/app-redirection.ts

+ 13
- 7
api/controllers/console/app/app.py 查看文件

parser.add_argument('icon_background', type=str, location='json') parser.add_argument('icon_background', type=str, location='json')
args = parser.parse_args() args = parser.parse_args()


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


if 'mode' not in args or args['mode'] is None: if 'mode' not in args or args['mode'] is None:
@cloud_edition_billing_resource_check('apps') @cloud_edition_billing_resource_check('apps')
def post(self): def post(self):
"""Import app""" """Import app"""
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@get_app_model @get_app_model
def delete(self, app_model): def delete(self, app_model):
"""Delete app""" """Delete app"""
if not current_user.is_admin_or_owner:
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


app_service = AppService() app_service = AppService()
@marshal_with(app_detail_fields_with_site) @marshal_with(app_detail_fields_with_site)
def post(self, app_model): def post(self, app_model):
"""Copy app""" """Copy app"""
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@get_app_model @get_app_model
@marshal_with(app_detail_fields) @marshal_with(app_detail_fields)
def post(self, app_model): def post(self, app_model):
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('enable_site', type=bool, required=True, location='json') parser.add_argument('enable_site', type=bool, required=True, location='json')
args = parser.parse_args() args = parser.parse_args()
@get_app_model @get_app_model
@marshal_with(app_detail_fields) @marshal_with(app_detail_fields)
def post(self, app_model): def post(self, app_model):
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('enable_api', type=bool, required=True, location='json') parser.add_argument('enable_api', type=bool, required=True, location='json')
args = parser.parse_args() args = parser.parse_args()

+ 13
- 1
api/controllers/console/app/conversation.py 查看文件

from flask_restful.inputs import int_range from flask_restful.inputs import int_range
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from werkzeug.exceptions import NotFound
from werkzeug.exceptions import Forbidden, NotFound


from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
@marshal_with(conversation_pagination_fields) @marshal_with(conversation_pagination_fields)
def get(self, app_model): def get(self, app_model):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('keyword', type=str, location='args') parser.add_argument('keyword', type=str, location='args')
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args') parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
@get_app_model(mode=AppMode.COMPLETION) @get_app_model(mode=AppMode.COMPLETION)
@marshal_with(conversation_message_detail_fields) @marshal_with(conversation_message_detail_fields)
def get(self, app_model, conversation_id): def get(self, app_model, conversation_id):
if not current_user.is_admin_or_owner:
raise Forbidden()
conversation_id = str(conversation_id) conversation_id = str(conversation_id)


return _get_conversation(app_model, conversation_id) return _get_conversation(app_model, conversation_id)
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
def delete(self, app_model, conversation_id): def delete(self, app_model, conversation_id):
if not current_user.is_admin_or_owner:
raise Forbidden()
conversation_id = str(conversation_id) conversation_id = str(conversation_id)


conversation = db.session.query(Conversation) \ conversation = db.session.query(Conversation) \
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
@marshal_with(conversation_with_summary_pagination_fields) @marshal_with(conversation_with_summary_pagination_fields)
def get(self, app_model): def get(self, app_model):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('keyword', type=str, location='args') parser.add_argument('keyword', type=str, location='args')
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args') parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
@marshal_with(conversation_detail_fields) @marshal_with(conversation_detail_fields)
def get(self, app_model, conversation_id): def get(self, app_model, conversation_id):
if not current_user.is_admin_or_owner:
raise Forbidden()
conversation_id = str(conversation_id) conversation_id = str(conversation_id)


return _get_conversation(app_model, conversation_id) return _get_conversation(app_model, conversation_id)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
@account_initialization_required @account_initialization_required
def delete(self, app_model, conversation_id): def delete(self, app_model, conversation_id):
if not current_user.is_admin_or_owner:
raise Forbidden()
conversation_id = str(conversation_id) conversation_id = str(conversation_id)


conversation = db.session.query(Conversation) \ conversation = db.session.query(Conversation) \

+ 2
- 2
api/controllers/console/app/site.py 查看文件

def post(self, app_model): def post(self, app_model):
args = parse_app_site_args() args = parse_app_site_args()


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be editor, admin, or owner
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


site = db.session.query(Site). \ site = db.session.query(Site). \

+ 6
- 6
api/controllers/console/datasets/datasets.py 查看文件

help='Invalid indexing technique.') help='Invalid indexing technique.')
args = parser.parse_args() args = parser.parse_args()


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


try: try:
parser.add_argument('retrieval_model', type=dict, location='json', help='Invalid retrieval model.') parser.add_argument('retrieval_model', type=dict, location='json', help='Invalid retrieval model.')
args = parser.parse_args() args = parser.parse_args()


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


dataset = DatasetService.update_dataset( dataset = DatasetService.update_dataset(
def delete(self, dataset_id): def delete(self, dataset_id):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


try: try:

+ 10
- 10
api/controllers/console/datasets/datasets_document.py 查看文件

if not dataset: if not dataset:
raise NotFound('Dataset not found.') raise NotFound('Dataset not found.')


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


try: try:
@marshal_with(dataset_and_document_fields) @marshal_with(dataset_and_document_fields)
@cloud_edition_billing_resource_check('vector_space') @cloud_edition_billing_resource_check('vector_space')
def post(self): def post(self):
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()
document_id = str(document_id) document_id = str(document_id)
document = self.get_document(dataset_id, document_id) document = self.get_document(dataset_id, document_id)


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


if action == "pause": if action == "pause":
doc_type = req_data.get('doc_type') doc_type = req_data.get('doc_type')
doc_metadata = req_data.get('doc_metadata') doc_metadata = req_data.get('doc_metadata')


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


if doc_type is None or doc_metadata is None: if doc_type is None or doc_metadata is None:


document = self.get_document(dataset_id, document_id) document = self.get_document(dataset_id, document_id)


# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


indexing_cache_key = 'document_{}_indexing'.format(document.id) indexing_cache_key = 'document_{}_indexing'.format(document.id)

+ 4
- 4
api/controllers/console/datasets/datasets_segments.py 查看文件

raise NotFound('Dataset not found.') raise NotFound('Dataset not found.')
# check user's model setting # check user's model setting
DatasetService.check_dataset_model_setting(dataset) DatasetService.check_dataset_model_setting(dataset)
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


try: try:
).first() ).first()
if not segment: if not segment:
raise NotFound('Segment not found.') raise NotFound('Segment not found.')
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()
try: try:
DatasetService.check_dataset_permission(dataset, current_user) DatasetService.check_dataset_permission(dataset, current_user)

+ 10
- 10
api/controllers/console/tag/tags.py 查看文件

@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@account_initialization_required @account_initialization_required
def patch(self, tag_id): def patch(self, tag_id):
tag_id = str(tag_id) tag_id = str(tag_id)
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@account_initialization_required @account_initialization_required
def delete(self, tag_id): def delete(self, tag_id):
tag_id = str(tag_id) tag_id = str(tag_id)
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


TagService.delete_tag(tag_id) TagService.delete_tag(tag_id)
@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
# The role of the current user in the ta table must be admin or owner
if not current_user.is_admin_or_owner:
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden() raise Forbidden()


parser = reqparse.RequestParser() parser = reqparse.RequestParser()

+ 2
- 2
api/controllers/console/workspace/members.py 查看文件

invitee_emails = args['emails'] invitee_emails = args['emails']
invitee_role = args['role'] invitee_role = args['role']
interface_language = args['language'] interface_language = args['language']
if invitee_role not in [TenantAccountRole.ADMIN, TenantAccountRole.NORMAL]:
if not TenantAccountRole.is_non_owner_role(invitee_role):
return {'code': 'invalid-role', 'message': 'Invalid role'}, 400 return {'code': 'invalid-role', 'message': 'Invalid role'}, 400


inviter = current_user inviter = current_user
args = parser.parse_args() args = parser.parse_args()
new_role = args['role'] new_role = args['role']


if new_role not in ['admin', 'normal', 'owner']:
if not TenantAccountRole.is_valid_role(new_role):
return {'code': 'invalid-role', 'message': 'Invalid role'}, 400 return {'code': 'invalid-role', 'message': 'Invalid role'}, 400


member = Account.query.get(str(member_id)) member = Account.query.get(str(member_id))

+ 5
- 3
api/controllers/console/workspace/models.py 查看文件

from core.model_runtime.errors.validate import CredentialsValidateFailedError from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from libs.login import login_required from libs.login import login_required
from models.account import TenantAccountRole
from services.model_load_balancing_service import ModelLoadBalancingService from services.model_load_balancing_service import ModelLoadBalancingService
from services.model_provider_service import ModelProviderService from services.model_provider_service import ModelProviderService


@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self): def post(self):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('model_settings', type=list, required=True, nullable=False, location='json') parser.add_argument('model_settings', type=list, required=True, nullable=False, location='json')
args = parser.parse_args() args = parser.parse_args()
@login_required @login_required
@account_initialization_required @account_initialization_required
def post(self, provider: str): def post(self, provider: str):
if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role):
if not current_user.is_admin_or_owner:
raise Forbidden() raise Forbidden()


tenant_id = current_user.current_tenant_id tenant_id = current_user.current_tenant_id
@login_required @login_required
@account_initialization_required @account_initialization_required
def delete(self, provider: str): def delete(self, provider: str):
if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role):
if not current_user.is_admin_or_owner:
raise Forbidden() raise Forbidden()


tenant_id = current_user.current_tenant_id tenant_id = current_user.current_tenant_id

+ 17
- 1
api/models/account.py 查看文件

def is_admin_or_owner(self): def is_admin_or_owner(self):
return TenantAccountRole.is_privileged_role(self._current_tenant.current_role) return TenantAccountRole.is_privileged_role(self._current_tenant.current_role)


@property
def is_editor(self):
return TenantAccountRole.is_editing_role(self._current_tenant.current_role)


class TenantStatus(str, enum.Enum): class TenantStatus(str, enum.Enum):
NORMAL = 'normal' NORMAL = 'normal'
class TenantAccountRole(str, enum.Enum): class TenantAccountRole(str, enum.Enum):
OWNER = 'owner' OWNER = 'owner'
ADMIN = 'admin' ADMIN = 'admin'
EDITOR = 'editor'
NORMAL = 'normal' NORMAL = 'normal'


@staticmethod
def is_valid_role(role: str) -> bool:
return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.NORMAL}

@staticmethod @staticmethod
def is_privileged_role(role: str) -> bool: def is_privileged_role(role: str) -> bool:
return role and role in {TenantAccountRole.ADMIN, TenantAccountRole.OWNER}
return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN}
@staticmethod
def is_non_owner_role(role: str) -> bool:
return role and role in {TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.NORMAL}
@staticmethod
def is_editing_role(role: str) -> bool:
return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}




class Tenant(db.Model): class Tenant(db.Model):

+ 2
- 0
api/tests/unit_tests/models/test_account.py 查看文件

def test_account_is_privileged_role() -> None: def test_account_is_privileged_role() -> None:
assert TenantAccountRole.ADMIN == 'admin' assert TenantAccountRole.ADMIN == 'admin'
assert TenantAccountRole.OWNER == 'owner' assert TenantAccountRole.OWNER == 'owner'
assert TenantAccountRole.EDITOR == 'editor'
assert TenantAccountRole.NORMAL == 'normal' assert TenantAccountRole.NORMAL == 'normal'


assert TenantAccountRole.is_privileged_role(TenantAccountRole.ADMIN) assert TenantAccountRole.is_privileged_role(TenantAccountRole.ADMIN)
assert TenantAccountRole.is_privileged_role(TenantAccountRole.OWNER) assert TenantAccountRole.is_privileged_role(TenantAccountRole.OWNER)
assert not TenantAccountRole.is_privileged_role(TenantAccountRole.NORMAL) assert not TenantAccountRole.is_privileged_role(TenantAccountRole.NORMAL)
assert not TenantAccountRole.is_privileged_role(TenantAccountRole.EDITOR)
assert not TenantAccountRole.is_privileged_role('') assert not TenantAccountRole.is_privileged_role('')

+ 16
- 13
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx 查看文件

const pathname = usePathname() const pathname = usePathname()
const media = useBreakpoints() const media = useBreakpoints()
const isMobile = media === MediaType.mobile const isMobile = media === MediaType.mobile
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({ const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
appDetail: state.appDetail, appDetail: state.appDetail,
setAppDetail: state.setAppDetail, setAppDetail: state.setAppDetail,
selectedIcon: NavIcon selectedIcon: NavIcon
}>>([]) }>>([])


const getNavigations = useCallback((appId: string, isCurrentWorkspaceManager: boolean, mode: string) => {
const getNavigations = useCallback((appId: string, isCurrentWorkspaceManager: boolean, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [ const navs = [
...(isCurrentWorkspaceManager
...(isCurrentWorkspaceEditor
? [{ ? [{
name: t('common.appMenus.promptEng'), name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`, href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
icon: TerminalSquare, icon: TerminalSquare,
selectedIcon: TerminalSquareSolid, selectedIcon: TerminalSquareSolid,
}, },
{
name: mode !== 'workflow'
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: FileHeart02,
selectedIcon: FileHeart02Solid,
},
...(isCurrentWorkspaceManager
? [{
name: mode !== 'workflow'
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: FileHeart02,
selectedIcon: FileHeart02Solid,
}]
: []
),
{ {
name: t('common.appMenus.overview'), name: t('common.appMenus.overview'),
href: `/app/${appId}/overview`, href: `/app/${appId}/overview`,
} }
else { else {
setAppDetail(res) setAppDetail(res)
setNavigation(getNavigations(appId, isCurrentWorkspaceManager, res.mode))
setNavigation(getNavigations(appId, isCurrentWorkspaceManager, isCurrentWorkspaceEditor, res.mode))
} }
}) })
}, [appId, isCurrentWorkspaceManager])
}, [appId, isCurrentWorkspaceManager, isCurrentWorkspaceEditor])


useUnmount(() => { useUnmount(() => {
setAppDetail() setAppDetail()

+ 4
- 4
web/app/(commonLayout)/apps/AppCard.tsx 查看文件

const AppCard = ({ app, onRefresh }: AppCardProps) => { const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const { onPlanInfoChanged } = useProviderContext() const { onPlanInfoChanged } = useProviderContext()
const { push } = useRouter() const { push } = useRouter()


onRefresh() onRefresh()
mutateApps() mutateApps()
onPlanInfoChanged() onPlanInfoChanged()
getRedirection(isCurrentWorkspaceManager, newApp, push)
getRedirection(isCurrentWorkspaceEditor, newApp, push)
} }
catch (e) { catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
<div <div
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
getRedirection(isCurrentWorkspaceManager, app, push)
getRedirection(isCurrentWorkspaceEditor, app, push)
}} }}
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
> >
/> />
</div> </div>
</div> </div>
{isCurrentWorkspaceManager && (
{isCurrentWorkspaceEditor && (
<> <>
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/> <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
<div className='!hidden group-hover:!flex shrink-0'> <div className='!hidden group-hover:!flex shrink-0'>

+ 2
- 2
web/app/(commonLayout)/apps/Apps.tsx 查看文件



const Apps = () => { const Apps = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal) const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const [activeTab, setActiveTab] = useTabSearchParams({ const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'all', defaultTab: 'all',
</div> </div>
</div> </div>
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{isCurrentWorkspaceManager
{isCurrentWorkspaceEditor
&& <NewAppCard onSuccess={mutate} />} && <NewAppCard onSuccess={mutate} />}
{data?.map(({ data: apps }: any) => apps.map((app: any) => ( {data?.map(({ data: apps }: any) => apps.map((app: any) => (
<AppCard key={app.id} app={app} onRefresh={mutate} /> <AppCard key={app.id} app={app} onRefresh={mutate} />

+ 2
- 2
web/app/(commonLayout)/datasets/Datasets.tsx 查看文件

tags, tags,
keywords, keywords,
}: Props) => { }: Props) => {
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const { data, isLoading, setSize, mutate } = useSWRInfinite( const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords), (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords),
fetchDatasets, fetchDatasets,


return ( return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{ isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> }
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
{data?.map(({ data: datasets }) => datasets.map(dataset => ( {data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />), <DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
))} ))}

+ 3
- 3
web/app/components/app/create-app-modal/index.tsx 查看文件



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)
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()


const isCreatingRef = useRef(false) const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = useCallback(async () => { const onCreate: MouseEventHandler = useCallback(async () => {
onClose() onClose()
mutateApps() mutateApps()
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceManager, app, push)
getRedirection(isCurrentWorkspaceEditor, app, push)
} }
catch (e) { catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
} }
isCreatingRef.current = false isCreatingRef.current = false
}, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceManager])
}, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor])


return ( return (
<Modal <Modal

+ 2
- 2
web/app/components/app/create-from-dsl-modal/index.tsx 查看文件

setFileContent('') setFileContent('')
} }


const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = 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)


onClose() onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') }) notify({ type: 'success', message: t('app.newApp.appCreated') })
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceManager, app, push)
getRedirection(isCurrentWorkspaceEditor, app, push)
} }
catch (e) { catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) notify({ type: 'error', message: t('app.newApp.appCreateFailed') })

+ 5
- 4
web/app/components/app/overview/appCard.tsx 查看文件

}: IAppCardProps) { }: IAppCardProps) {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
const { currentWorkspace, isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
const [showSettingsModal, setShowSettingsModal] = useState(false) const [showSettingsModal, setShowSettingsModal] = useState(false)
const [showEmbedded, setShowEmbedded] = useState(false) const [showEmbedded, setShowEmbedded] = useState(false)
const [showCustomizeModal, setShowCustomizeModal] = useState(false) const [showCustomizeModal, setShowCustomizeModal] = useState(false)
if (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') if (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow')
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon }) operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon })


if (isCurrentWorkspaceManager)
if (isCurrentWorkspaceEditor)
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon }) operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon })


return operationsMap return operationsMap
}, [isCurrentWorkspaceManager, appInfo, t])
}, [isCurrentWorkspaceEditor, appInfo, t])


const isApp = cardType === 'webapp' const isApp = cardType === 'webapp'
const basicName = isApp const basicName = isApp
? appInfo?.site?.title ? appInfo?.site?.title
: t('appOverview.overview.apiInfo.title') : t('appOverview.overview.apiInfo.title')
const toggleDisabled = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
const { app_base_url, access_token } = appInfo.site ?? {} const { app_base_url, access_token } = appInfo.site ?? {}
const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode
? t('appOverview.overview.status.running') ? t('appOverview.overview.status.running')
: t('appOverview.overview.status.disable')} : t('appOverview.overview.status.disable')}
</Tag> </Tag>
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} />
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} />
</div> </div>
</div> </div>
<div className="flex flex-col justify-center py-2"> <div className="flex flex-col justify-center py-2">

+ 2
- 2
web/app/components/app/switch-app-modal/index.tsx 查看文件

const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const setAppDetail = useAppStore(s => s.setAppDetail) const setAppDetail = useAppStore(s => s.setAppDetail)


const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = 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)


await deleteApp(appDetail.id) await deleteApp(appDetail.id)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection( getRedirection(
isCurrentWorkspaceManager,
isCurrentWorkspaceEditor,
{ {
id: newAppID, id: newAppID,
mode: appDetail.mode === 'completion' ? 'workflow' : 'advanced-chat', mode: appDetail.mode === 'completion' ? 'workflow' : 'advanced-chat',

+ 1
- 1
web/app/components/develop/secret-key/secret-key-modal.tsx 查看文件

) )
} }
<div className='flex'> <div className='flex'>
<Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || currentWorkspace.role === 'normal'}>
<Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || !isCurrentWorkspaceManager}>
<PlusIcon className='flex flex-shrink-0 w-4 h-4' /> <PlusIcon className='flex flex-shrink-0 w-4 h-4' />
<div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div> <div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div>
</Button> </Button>

+ 2
- 2
web/app/components/explore/app-list/index.tsx 查看文件

onSuccess, onSuccess,
}: AppsProps) => { }: AppsProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const { push } = useRouter() const { push } = useRouter()
const { hasEditPermission } = useContext(ExploreContext) const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' }) const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
if (onSuccess) if (onSuccess)
onSuccess() onSuccess()
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceManager, app, push)
getRedirection(isCurrentWorkspaceEditor, app, push)
} }
catch (e) { catch (e) {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })

+ 1
- 0
web/app/components/header/account-setting/members-page/index.tsx 查看文件

const RoleMap = { const RoleMap = {
owner: t('common.members.owner'), owner: t('common.members.owner'),
admin: t('common.members.admin'), admin: t('common.members.admin'),
editor: t('common.members.editor'),
normal: t('common.members.normal'), normal: t('common.members.normal'),
} }
const { locale } = useContext(I18n) const { locale } = useContext(I18n)

+ 6
- 2
web/app/components/header/account-setting/members-page/invite-modal/index.tsx 查看文件

name: 'normal', name: 'normal',
description: t('common.members.normalTip'), description: t('common.members.normalTip'),
}, },
{
name: 'editor',
description: t('common.members.editorTip'),
},
{ {
name: 'admin', name: 'admin',
description: t('common.members.adminTip'), description: t('common.members.adminTip'),
<div className='flex flex-row'> <div className='flex flex-row'>
<span <span
className={cn( className={cn(
'text-indigo-600 w-8',
'text-indigo-600 mr-2',
'flex items-center', 'flex items-center',
)} )}
> >
<span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block truncate`}> <span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block truncate`}>
{t(`common.members.${role.name}`)} {t(`common.members.${role.name}`)}
</span> </span>
<span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block truncate`}>
<span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block text-gray-500`}>
{role.description} {role.description}
</span> </span>
</div> </div>

+ 2
- 1
web/app/components/header/account-setting/members-page/operation/index.tsx 查看文件

const RoleMap = { const RoleMap = {
owner: t('common.members.owner'), owner: t('common.members.owner'),
admin: t('common.members.admin'), admin: t('common.members.admin'),
editor: t('common.members.editor'),
normal: t('common.members.normal'), normal: t('common.members.normal'),
} }
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
> >
<div className="px-1 py-1"> <div className="px-1 py-1">
{ {
['admin', 'normal'].map(role => (
['admin', 'editor', 'normal'].map(role => (
<Menu.Item key={role}> <Menu.Item key={role}>
<div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}> <div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}>
{ {

+ 4
- 1
web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx 查看文件

} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import ConfirmCommon from '@/app/components/base/confirm/common' import ConfirmCommon from '@/app/components/base/confirm/common'
import { useAppContext } from '@/context/app-context'


type ModelModalProps = { type ModelModalProps = {
provider: ModelProvider provider: ModelProvider
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
) )
const isEditMode = !!formSchemasValue
const { isCurrentWorkspaceManager } = useAppContext()
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const language = useLanguage() const language = useLanguage()
|| filteredRequiredFormSchemas.some(item => value[item.variable] === undefined) || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2) || (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
} }

> >
{t('common.operation.save')} {t('common.operation.save')}
</Button> </Button>

+ 3
- 0
web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx 查看文件

import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { updateDefaultModel } from '@/service/common' import { updateDefaultModel } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'


type SystemModelSelectorProps = { type SystemModelSelectorProps = {
textGenerationDefaultModel: DefaultModelResponse | undefined textGenerationDefaultModel: DefaultModelResponse | undefined
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const { isCurrentWorkspaceManager } = useAppContext()
const { textGenerationModelList } = useProviderContext() const { textGenerationModelList } = useProviderContext()
const updateModelList = useUpdateModelList() const updateModelList = useUpdateModelList()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
type='primary' type='primary'
className='!h-8 !text-[13px]' className='!h-8 !text-[13px]'
onClick={handleSave} onClick={handleSave}
disabled={!isCurrentWorkspaceManager}
> >
{t('common.operation.save')} {t('common.operation.save')}
</Button> </Button>

+ 5
- 5
web/app/components/header/app-nav/index.tsx 查看文件

const AppNav = () => { const AppNav = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { appId } = useParams() const { appId } = useParams()
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const [showNewAppDialog, setShowNewAppDialog] = useState(false) const [showNewAppDialog, setShowNewAppDialog] = useState(false)
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false) const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
if (appsData) { if (appsData) {
const appItems = flatten(appsData?.map(appData => appData.data)) const appItems = flatten(appsData?.map(appData => appData.data))
const navItems = appItems.map((app) => { const navItems = appItems.map((app) => {
const link = ((isCurrentWorkspaceManager, app) => {
if (!isCurrentWorkspaceManager) {
const link = ((isCurrentWorkspaceEditor, app) => {
if (!isCurrentWorkspaceEditor) {
return `/app/${app.id}/overview` return `/app/${app.id}/overview`
} }
else { else {
else else
return `/app/${app.id}/configuration` return `/app/${app.id}/configuration`
} }
})(isCurrentWorkspaceManager, app)
})(isCurrentWorkspaceEditor, app)
return { return {
id: app.id, id: app.id,
icon: app.icon, icon: app.icon,
}) })
setNavItems(navItems) setNavItems(navItems)
} }
}, [appsData, isCurrentWorkspaceManager, setNavItems])
}, [appsData, isCurrentWorkspaceEditor, setNavItems])


// update current app name // update current app name
useEffect(() => { useEffect(() => {

+ 3
- 3
web/app/components/header/app-selector/index.tsx 查看文件



export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
const router = useRouter() const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const [showNewAppDialog, setShowNewAppDialog] = useState(false) const [showNewAppDialog, setShowNewAppDialog] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()


appItems.map((app: AppDetailResponse) => ( appItems.map((app: AppDetailResponse) => (
<Menu.Item key={app.id}> <Menu.Item key={app.id}>
<div className={itemClassName} onClick={() => <div className={itemClassName} onClick={() =>
router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
router.push(`/app/${app.id}/${isCurrentWorkspaceEditor ? 'configuration' : 'overview'}`)
}> }>
<div className='relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-[6px]'> <div className='relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-[6px]'>
<AppIcon size='tiny' /> <AppIcon size='tiny' />
)) ))
} }
</div>)} </div>)}
{isCurrentWorkspaceManager && <Menu.Item>
{isCurrentWorkspaceEditor && <Menu.Item>
<div className='p-1' onClick={() => setShowNewAppDialog(true)}> <div className='p-1' onClick={() => setShowNewAppDialog(true)}>
<div <div
className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100'

+ 3
- 3
web/app/components/header/index.tsx 查看文件

` `


const Header = () => { const Header = () => {
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()


const selectedSegment = useSelectedLayoutSegment() const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints() const media = useBreakpoints()
<div className='flex items-center'> <div className='flex items-center'>
<ExploreNav className={navClassName} /> <ExploreNav className={navClassName} />
<AppNav /> <AppNav />
{isCurrentWorkspaceManager && <DatasetNav />}
{isCurrentWorkspaceEditor && <DatasetNav />}
<ToolsNav className={navClassName} /> <ToolsNav className={navClassName} />
</div> </div>
)} )}
<div className='w-full flex flex-col p-2 gap-y-1'> <div className='w-full flex flex-col p-2 gap-y-1'>
<ExploreNav className={navClassName} /> <ExploreNav className={navClassName} />
<AppNav /> <AppNav />
{isCurrentWorkspaceManager && <DatasetNav />}
{isCurrentWorkspaceEditor && <DatasetNav />}
<ToolsNav className={navClassName} /> <ToolsNav className={navClassName} />
</div> </div>
)} )}

+ 2
- 2
web/app/components/header/nav/nav-selector/index.tsx 查看文件

const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: INavSelectorProps) => { const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: INavSelectorProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const { isCurrentWorkspaceEditor } = useAppContext()
const setAppDetail = useAppStore(state => state.setAppDetail) const setAppDetail = useAppStore(state => state.setAppDetail)


const handleScroll = useCallback(debounce((e) => { const handleScroll = useCallback(debounce((e) => {
</div> </div>
</Menu.Button> </Menu.Button>
)} )}
{isApp && isCurrentWorkspaceManager && (
{isApp && isCurrentWorkspaceEditor && (
<Menu as="div" className="relative w-full h-full"> <Menu as="div" className="relative w-full h-full">
{({ open }) => ( {({ open }) => (
<> <>

+ 18
- 14
web/app/components/tools/provider/custom-create-card.tsx 查看文件

import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import { createCustomCollection } from '@/service/tools' import { createCustomCollection } from '@/service/tools'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'


type Props = { type Props = {
onRefreshData: () => void onRefreshData: () => void
const { t } = useTranslation() const { t } = useTranslation()
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const language = getLanguage(locale) const language = getLanguage(locale)
const { isCurrentWorkspaceManager } = useAppContext()


const linkUrl = useMemo(() => { const linkUrl = useMemo(() => {
if (language.startsWith('zh_')) if (language.startsWith('zh_'))


return ( return (
<> <>
<div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='shrink-0 flex items-center p-4 pb-3'>
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'>
<Plus className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/>
{isCurrentWorkspaceManager && (
<div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='shrink-0 flex items-center p-4 pb-3'>
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'>
<Plus className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/>
</div>
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div>
</div> </div>
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div>
</div>
<div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<BookOpen01 className='shrink-0 w-3 h-3' />
<div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
<ArrowUpRight className='shrink-0 w-3 h-3' />
</a>
</div> </div>
</div> </div>
<div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<BookOpen01 className='shrink-0 w-3 h-3' />
<div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
<ArrowUpRight className='shrink-0 w-3 h-3' />
</a>
</div>
</div>
)}
{isShowEditCollectionToolModal && ( {isShowEditCollectionToolModal && (
<EditCustomToolModal <EditCustomToolModal
payload={null} payload={null}

+ 4
- 0
web/app/components/tools/provider/detail.tsx 查看文件

import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'


type Props = { type Props = {
collection: Collection collection: Collection
const isAuthed = collection.is_team_authorization const isAuthed = collection.is_team_authorization
const isBuiltIn = collection.type === CollectionType.builtIn const isBuiltIn = collection.type === CollectionType.builtIn
const isModel = collection.type === CollectionType.model const isModel = collection.type === CollectionType.model
const { isCurrentWorkspaceManager } = useAppContext()


const [isDetailLoading, setIsDetailLoading] = useState(false) const [isDetailLoading, setIsDetailLoading] = useState(false)


if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model) if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
showSettingAuthModal() showSettingAuthModal()
}} }}
disabled={!isCurrentWorkspaceManager}
> >
{isAuthed && <Indicator className='mr-2' color={'green'} />} {isAuthed && <Indicator className='mr-2' color={'green'} />}
<div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}> <div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}>
<Button <Button
className={cn('shrink-0 my-3 w-[183px] flex items-center bg-white')} className={cn('shrink-0 my-3 w-[183px] flex items-center bg-white')}
onClick={() => setIsShowEditWorkflowToolModal(true)} onClick={() => setIsShowEditWorkflowToolModal(true)}
disabled={!isCurrentWorkspaceManager}
> >
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div> <div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
</Button> </Button>

+ 2
- 0
web/app/components/tools/setting/build-in/config-credentials.tsx 查看文件

import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { useAppContext } from '@/context/app-context'


type Props = { type Props = {
collection: Collection collection: Collection
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [credentialSchema, setCredentialSchema] = useState<any>(null) const [credentialSchema, setCredentialSchema] = useState<any>(null)
const { isCurrentWorkspaceManager } = useAppContext()
const { name: collectionName } = collection const { name: collectionName } = collection
const [tempCredential, setTempCredential] = React.useState<any>({}) const [tempCredential, setTempCredential] = React.useState<any>({})
useEffect(() => { useEffect(() => {

+ 23
- 10
web/app/components/tools/workflow-tool/configure-button.tsx 查看文件

import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools' import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools'
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types' import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'


type Props = { type Props = {
disabled: boolean disabled: boolean
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [detail, setDetail] = useState<WorkflowToolProviderResponse>() const [detail, setDetail] = useState<WorkflowToolProviderResponse>()
const { isCurrentWorkspaceManager } = useAppContext()


const outdated = useMemo(() => { const outdated = useMemo(() => {
if (!detail) if (!detail)
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer', disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer',
!published && 'hover:bg-primary-50', !published && 'hover:bg-primary-50',
)}> )}>
<div
className='flex justify-start items-center gap-2 px-2.5 py-2'
onClick={() => !published && setShowModal(true)}
>
<Tools className={cn('relative w-4 h-4', !published && 'group-hover:text-primary-600')}/>
<div title={t('workflow.common.workflowAsTool') || ''} className={cn('grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate', !published && 'group-hover:text-primary-600')}>{t('workflow.common.workflowAsTool')}</div>
{!published && (
<span className='shrink-0 px-1 border border-black/8 rounded-[5px] bg-white text-[10px] font-medium leading-[18px] text-gray-500'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span>
{isCurrentWorkspaceManager
? (
<div
className='flex justify-start items-center gap-2 px-2.5 py-2'
onClick={() => !published && setShowModal(true)}
>
<Tools className={cn('relative w-4 h-4', !published && 'group-hover:text-primary-600')} />
<div title={t('workflow.common.workflowAsTool') || ''} className={cn('grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate', !published && 'group-hover:text-primary-600')}>{t('workflow.common.workflowAsTool')}</div>
{!published && (
<span className='shrink-0 px-1 border border-black/8 rounded-[5px] bg-white text-[10px] font-medium leading-[18px] text-gray-500'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span>
)}
</div>)
: (
<div
className='flex justify-start items-center gap-2 px-2.5 py-2'
>
<Tools className='w-4 h-4 text-gray-500' />
<div title={t('workflow.common.workflowAsTool') || ''} className='grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate text-gray-500'>{t('workflow.common.workflowAsTool')}</div>
</div>
)} )}
</div>
{published && ( {published && (
<div className='px-2.5 py-2 border-t-[0.5px] border-black/5'> <div className='px-2.5 py-2 border-t-[0.5px] border-black/5'>
<div className='flex justify-between'> <div className='flex justify-between'>
<Button <Button
className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200' className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200'
onClick={() => setShowModal(true)} onClick={() => setShowModal(true)}
disabled={!isCurrentWorkspaceManager}
> >
{t('workflow.common.configure')} {t('workflow.common.configure')}
{outdated && <Indicator className='ml-1' color={'yellow'} />} {outdated && <Indicator className='ml-1' color={'yellow'} />}
)} )}
</div> </div>
)} )}
{published && isLoading && <div className='pt-2'><Loading type='app'/></div>}
{published && isLoading && <div className='pt-2'><Loading type='app' /></div>}
</div> </div>
{showModal && ( {showModal && (
<WorkflowToolModal <WorkflowToolModal

+ 4
- 0
web/context/app-context.tsx 查看文件

currentWorkspace: ICurrentWorkspace currentWorkspace: ICurrentWorkspace
isCurrentWorkspaceManager: boolean isCurrentWorkspaceManager: boolean
isCurrentWorkspaceOwner: boolean isCurrentWorkspaceOwner: boolean
isCurrentWorkspaceEditor: boolean
mutateCurrentWorkspace: VoidFunction mutateCurrentWorkspace: VoidFunction
pageContainerRef: React.RefObject<HTMLDivElement> pageContainerRef: React.RefObject<HTMLDivElement>
langeniusVersionInfo: LangGeniusVersionResponse langeniusVersionInfo: LangGeniusVersionResponse
currentWorkspace: initialWorkspaceInfo, currentWorkspace: initialWorkspaceInfo,
isCurrentWorkspaceManager: false, isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false, isCurrentWorkspaceOwner: false,
isCurrentWorkspaceEditor: false,
mutateUserProfile: () => { }, mutateUserProfile: () => { },
mutateCurrentWorkspace: () => { }, mutateCurrentWorkspace: () => { },
pageContainerRef: createRef(), pageContainerRef: createRef(),
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role]) const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role]) const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
const updateUserProfileAndVersion = useCallback(async () => { const updateUserProfileAndVersion = useCallback(async () => {
if (userProfileResponse && !userProfileResponse.bodyUsed) { if (userProfileResponse && !userProfileResponse.bodyUsed) {
const result = await userProfileResponse.json() const result = await userProfileResponse.json()
currentWorkspace, currentWorkspace,
isCurrentWorkspaceManager, isCurrentWorkspaceManager,
isCurrentWorkspaceOwner, isCurrentWorkspaceOwner,
isCurrentWorkspaceEditor,
mutateCurrentWorkspace, mutateCurrentWorkspace,
}}> }}>
<div className='flex flex-col h-full overflow-y-auto'> <div className='flex flex-col h-full overflow-y-auto'>

+ 3
- 0
web/i18n/de-DE/common.ts 查看文件

adminTip: 'Kann Apps erstellen & Team-Einstellungen verwalten', adminTip: 'Kann Apps erstellen & Team-Einstellungen verwalten',
normal: 'Normal', normal: 'Normal',
normalTip: 'Kann nur Apps verwenden, kann keine Apps erstellen', normalTip: 'Kann nur Apps verwenden, kann keine Apps erstellen',
editor: 'Editor',
editorTip: 'Kann Apps erstellen & bearbeiten',
inviteTeamMember: 'Teammitglied hinzufügen', inviteTeamMember: 'Teammitglied hinzufügen',
inviteTeamMemberTip: 'Sie können direkt nach der Anmeldung auf Ihre Teamdaten zugreifen.', inviteTeamMemberTip: 'Sie können direkt nach der Anmeldung auf Ihre Teamdaten zugreifen.',
email: 'E-Mail', email: 'E-Mail',
removeFromTeamTip: 'Wird den Teamzugang entfernen', removeFromTeamTip: 'Wird den Teamzugang entfernen',
setAdmin: 'Als Administrator einstellen', setAdmin: 'Als Administrator einstellen',
setMember: 'Als normales Mitglied einstellen', setMember: 'Als normales Mitglied einstellen',
setEditor: 'Als Editor einstellen',
disinvite: 'Einladung widerrufen', disinvite: 'Einladung widerrufen',
deleteMember: 'Mitglied löschen', deleteMember: 'Mitglied löschen',
you: '(Du)', you: '(Du)',

+ 6
- 0
web/i18n/en-US/common.ts 查看文件

adminTip: 'Can build apps & manage team settings', adminTip: 'Can build apps & manage team settings',
normal: 'Normal', normal: 'Normal',
normalTip: 'Only can use apps, can not build apps', normalTip: 'Only can use apps, can not build apps',
builder: 'Builder',
builderTip: 'Can build & edit own apps',
editor: 'Editor',
editorTip: 'Can build & edit apps',
inviteTeamMember: 'Add team member', inviteTeamMember: 'Add team member',
inviteTeamMemberTip: 'They can access your team data directly after signing in.', inviteTeamMemberTip: 'They can access your team data directly after signing in.',
email: 'Email', email: 'Email',
removeFromTeamTip: 'Will remove team access', removeFromTeamTip: 'Will remove team access',
setAdmin: 'Set as administrator', setAdmin: 'Set as administrator',
setMember: 'Set to ordinary member', setMember: 'Set to ordinary member',
setBuilder: 'Set as builder',
setEditor: 'Set as editor',
disinvite: 'Cancel the invitation', disinvite: 'Cancel the invitation',
deleteMember: 'Delete Member', deleteMember: 'Delete Member',
you: '(You)', you: '(You)',

+ 3
- 0
web/i18n/fr-FR/common.ts 查看文件

adminTip: 'Peut construire des applications & gérer les paramètres de l\'équipe', adminTip: 'Peut construire des applications & gérer les paramètres de l\'équipe',
normal: 'Normal', normal: 'Normal',
normalTip: 'Peut seulement utiliser des applications, ne peut pas construire des applications', normalTip: 'Peut seulement utiliser des applications, ne peut pas construire des applications',
editor: 'Éditeur',
editorTip: 'Peut construire des applications, mais ne peut pas gérer les paramètres de l\'équipe',
inviteTeamMember: 'Ajouter un membre de l\'équipe', inviteTeamMember: 'Ajouter un membre de l\'équipe',
inviteTeamMemberTip: 'Ils peuvent accéder directement à vos données d\'équipe après s\'être connectés.', inviteTeamMemberTip: 'Ils peuvent accéder directement à vos données d\'équipe après s\'être connectés.',
email: 'Courrier électronique', email: 'Courrier électronique',
removeFromTeamTip: 'Supprimera l\'accès de l\'équipe', removeFromTeamTip: 'Supprimera l\'accès de l\'équipe',
setAdmin: 'Définir comme administrateur', setAdmin: 'Définir comme administrateur',
setMember: 'Définir en tant que membre ordinaire', setMember: 'Définir en tant que membre ordinaire',
setEditor: 'Définir en tant qu\'éditeur',
disinvite: 'Annuler l\'invitation', disinvite: 'Annuler l\'invitation',
deleteMember: 'Supprimer Membre', deleteMember: 'Supprimer Membre',
you: '(Vous)', you: '(Vous)',

+ 3
- 0
web/i18n/ja-JP/common.ts 查看文件

adminTip: 'アプリの構築およびチーム設定の管理ができます', adminTip: 'アプリの構築およびチーム設定の管理ができます',
normal: '通常', normal: '通常',
normalTip: 'アプリの使用のみが可能で、アプリの構築はできません', normalTip: 'アプリの使用のみが可能で、アプリの構築はできません',
editor: 'エディター',
editorTip: 'アプリの構築ができますが、チーム設定の管理はできません',
inviteTeamMember: 'チームメンバーを招待する', inviteTeamMember: 'チームメンバーを招待する',
inviteTeamMemberTip: '彼らはサインイン後、直接あなたのチームデータにアクセスできます。', inviteTeamMemberTip: '彼らはサインイン後、直接あなたのチームデータにアクセスできます。',
email: 'メール', email: 'メール',
removeFromTeamTip: 'チームへのアクセスが削除されます', removeFromTeamTip: 'チームへのアクセスが削除されます',
setAdmin: '管理者に設定', setAdmin: '管理者に設定',
setMember: '通常のメンバーに設定', setMember: '通常のメンバーに設定',
setEditor: 'エディターに設定',
disinvite: '招待をキャンセル', disinvite: '招待をキャンセル',
deleteMember: 'メンバーを削除', deleteMember: 'メンバーを削除',
you: '(あなた)', you: '(あなた)',

+ 3
- 0
web/i18n/ko-KR/common.ts 查看文件

adminTip: '앱 빌드 및 팀 설정 관리 가능', adminTip: '앱 빌드 및 팀 설정 관리 가능',
normal: '일반', normal: '일반',
normalTip: '앱 사용만 가능하고 앱 빌드는 불가능', normalTip: '앱 사용만 가능하고 앱 빌드는 불가능',
editor: '편집자',
editorTip: '앱 빌드만 가능하고 팀 설정 관리 불가능',
inviteTeamMember: '팀 멤버 초대', inviteTeamMember: '팀 멤버 초대',
inviteTeamMemberTip: '로그인 후에 바로 팀 데이터에 액세스할 수 있습니다.', inviteTeamMemberTip: '로그인 후에 바로 팀 데이터에 액세스할 수 있습니다.',
email: '이메일', email: '이메일',
removeFromTeamTip: '팀 액세스가 제거됩니다', removeFromTeamTip: '팀 액세스가 제거됩니다',
setAdmin: '관리자 설정', setAdmin: '관리자 설정',
setMember: '일반 멤버 설정', setMember: '일반 멤버 설정',
setEditor: '편집자 설정',
disinvite: '초대 취소', disinvite: '초대 취소',
deleteMember: '멤버 삭제', deleteMember: '멤버 삭제',
you: '(나)', you: '(나)',

+ 3
- 0
web/i18n/pl-PL/common.ts 查看文件

adminTip: 'Może tworzyć aplikacje i zarządzać ustawieniami zespołu', adminTip: 'Może tworzyć aplikacje i zarządzać ustawieniami zespołu',
normal: 'Normalny', normal: 'Normalny',
normalTip: 'Może tylko korzystać z aplikacji, nie może tworzyć aplikacji', normalTip: 'Może tylko korzystać z aplikacji, nie może tworzyć aplikacji',
editor: 'Edytor',
editorTip: 'Może tworzyć i edytować aplikacje, ale nie zarządzać ustawieniami zespołu',
inviteTeamMember: 'Dodaj członka zespołu', inviteTeamMember: 'Dodaj członka zespołu',
inviteTeamMemberTip: inviteTeamMemberTip:
'Mogą uzyskać bezpośredni dostęp do danych Twojego zespołu po zalogowaniu.', 'Mogą uzyskać bezpośredni dostęp do danych Twojego zespołu po zalogowaniu.',
removeFromTeamTip: 'Usunie dostęp do zespołu', removeFromTeamTip: 'Usunie dostęp do zespołu',
setAdmin: 'Ustaw jako administratora', setAdmin: 'Ustaw jako administratora',
setMember: 'Ustaw jako zwykłego członka', setMember: 'Ustaw jako zwykłego członka',
setEditor: 'Ustaw jako edytora',
disinvite: 'Anuluj zaproszenie', disinvite: 'Anuluj zaproszenie',
deleteMember: 'Usuń członka', deleteMember: 'Usuń członka',
you: '(Ty)', you: '(Ty)',

+ 3
- 0
web/i18n/pt-BR/common.ts 查看文件

adminTip: 'Pode criar aplicativos e gerenciar configurações da equipe', adminTip: 'Pode criar aplicativos e gerenciar configurações da equipe',
normal: 'Normal', normal: 'Normal',
normalTip: 'Só pode usar aplicativos, não pode criar aplicativos', normalTip: 'Só pode usar aplicativos, não pode criar aplicativos',
editor: 'Editor',
editorTip: 'Pode editar aplicativos, mas não pode gerenciar configurações da equipe',
inviteTeamMember: 'Adicionar membro da equipe', inviteTeamMember: 'Adicionar membro da equipe',
inviteTeamMemberTip: 'Eles podem acessar os dados da sua equipe diretamente após fazer login.', inviteTeamMemberTip: 'Eles podem acessar os dados da sua equipe diretamente após fazer login.',
email: 'E-mail', email: 'E-mail',
removeFromTeamTip: 'Removerá o acesso da equipe', removeFromTeamTip: 'Removerá o acesso da equipe',
setAdmin: 'Definir como administrador', setAdmin: 'Definir como administrador',
setMember: 'Definir como membro comum', setMember: 'Definir como membro comum',
setEditor: 'Definir como editor',
disinvite: 'Cancelar o convite', disinvite: 'Cancelar o convite',
deleteMember: 'Excluir Membro', deleteMember: 'Excluir Membro',
you: '(Você)', you: '(Você)',

+ 3
- 0
web/i18n/ro-RO/common.ts 查看文件

adminTip: 'Poate construi aplicații și gestiona setările echipei', adminTip: 'Poate construi aplicații și gestiona setările echipei',
normal: 'Normal', normal: 'Normal',
normalTip: 'Poate doar utiliza aplicații, nu poate construi aplicații', normalTip: 'Poate doar utiliza aplicații, nu poate construi aplicații',
editor: 'Editor',
editorTip: 'Poate construi aplicații, dar nu poate gestiona setările echipei',
inviteTeamMember: 'Adaugă membru în echipă', inviteTeamMember: 'Adaugă membru în echipă',
inviteTeamMemberTip: 'Pot accesa direct datele echipei dvs. după autentificare.', inviteTeamMemberTip: 'Pot accesa direct datele echipei dvs. după autentificare.',
email: 'Email', email: 'Email',
removeFromTeamTip: 'Va elimina accesul la echipă', removeFromTeamTip: 'Va elimina accesul la echipă',
setAdmin: 'Setează ca administrator', setAdmin: 'Setează ca administrator',
setMember: 'Setează ca membru obișnuit', setMember: 'Setează ca membru obișnuit',
setEditor: 'Setează ca editor',
disinvite: 'Anulează invitația', disinvite: 'Anulează invitația',
deleteMember: 'Șterge membru', deleteMember: 'Șterge membru',
you: '(Dvs.)', you: '(Dvs.)',

+ 3
- 0
web/i18n/uk-UA/common.ts 查看文件

adminTip: 'Може створювати програми та керувати налаштуваннями команди', adminTip: 'Може створювати програми та керувати налаштуваннями команди',
normal: 'Звичайний', normal: 'Звичайний',
normalTip: 'Може лише використовувати програми, не може створювати програми', normalTip: 'Може лише використовувати програми, не може створювати програми',
editor: 'Редактор',
editorTip: 'Може створювати програми, але не може керувати налаштуваннями команди',
inviteTeamMember: 'Додати учасника команди', inviteTeamMember: 'Додати учасника команди',
inviteTeamMemberTip: 'Вони зможуть отримати доступ до даних вашої команди безпосередньо після входу.', inviteTeamMemberTip: 'Вони зможуть отримати доступ до даних вашої команди безпосередньо після входу.',
email: 'Електронна пошта', email: 'Електронна пошта',
removeFromTeamTip: 'Буде видалено доступ до команди', removeFromTeamTip: 'Буде видалено доступ до команди',
setAdmin: 'Призначити адміністратором', setAdmin: 'Призначити адміністратором',
setMember: 'Встановити як звичайного члена', setMember: 'Встановити як звичайного члена',
setEditor: 'Встановити як Редактор',
disinvite: 'Скасувати запрошення', disinvite: 'Скасувати запрошення',
deleteMember: 'Видалити учасника', deleteMember: 'Видалити учасника',
you: '(Ви)', you: '(Ви)',

+ 3
- 0
web/i18n/vi-VN/common.ts 查看文件

adminTip: 'Có thể xây dựng ứng dụng và quản lý cài đặt nhóm', adminTip: 'Có thể xây dựng ứng dụng và quản lý cài đặt nhóm',
normal: 'Bình thường', normal: 'Bình thường',
normalTip: 'Chỉ có thể sử dụng ứng dụng, không thể xây dựng ứng dụng', normalTip: 'Chỉ có thể sử dụng ứng dụng, không thể xây dựng ứng dụng',
editor: 'Biên tập viên',
editorTip: 'Chỉ có thể xây dựng ứng dụng, không thể quản lý cài đặt nhóm',
inviteTeamMember: 'Mời thành viên nhóm', inviteTeamMember: 'Mời thành viên nhóm',
inviteTeamMemberTip: 'Sau khi đăng nhập, họ có thể truy cập trực tiếp vào dữ liệu nhóm của bạn.', inviteTeamMemberTip: 'Sau khi đăng nhập, họ có thể truy cập trực tiếp vào dữ liệu nhóm của bạn.',
email: 'Email', email: 'Email',
removeFromTeamTip: 'Sẽ xóa quyền truy cập nhóm', removeFromTeamTip: 'Sẽ xóa quyền truy cập nhóm',
setAdmin: 'Đặt làm quản trị viên', setAdmin: 'Đặt làm quản trị viên',
setMember: 'Đặt thành viên bình thường', setMember: 'Đặt thành viên bình thường',
setEditor: 'Đặt làm biên tập viên',
disinvite: 'Hủy lời mời', disinvite: 'Hủy lời mời',
deleteMember: 'Xóa thành viên', deleteMember: 'Xóa thành viên',
you: '(Bạn)', you: '(Bạn)',

+ 3
- 0
web/i18n/zh-Hans/common.ts 查看文件

adminTip: '能够建立应用程序和管理团队设置', adminTip: '能够建立应用程序和管理团队设置',
normal: '成员', normal: '成员',
normalTip: '只能使用应用程序,不能建立应用程序', normalTip: '只能使用应用程序,不能建立应用程序',
editor: '编辑',
editorTip: '能够建立并编辑应用程序,不能管理团队设置',
inviteTeamMember: '添加团队成员', inviteTeamMember: '添加团队成员',
inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。', inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。',
email: '邮箱', email: '邮箱',
removeFromTeamTip: '将取消团队访问', removeFromTeamTip: '将取消团队访问',
setAdmin: '设为管理员', setAdmin: '设为管理员',
setMember: '设为普通成员', setMember: '设为普通成员',
setEditor: '设为编辑',
disinvite: '取消邀请', disinvite: '取消邀请',
deleteMember: '删除成员', deleteMember: '删除成员',
you: '(你)', you: '(你)',

+ 3
- 0
web/i18n/zh-Hant/common.ts 查看文件

adminTip: '能夠建立應用程式和管理團隊設定', adminTip: '能夠建立應用程式和管理團隊設定',
normal: '成員', normal: '成員',
normalTip: '只能使用應用程式,不能建立應用程式', normalTip: '只能使用應用程式,不能建立應用程式',
editor: '編輯',
editorTip: '能夠建立並編輯應用程式,不能管理團隊設定',
inviteTeamMember: '新增團隊成員', inviteTeamMember: '新增團隊成員',
inviteTeamMemberTip: '對方在登入後可以訪問你的團隊資料。', inviteTeamMemberTip: '對方在登入後可以訪問你的團隊資料。',
email: '郵箱', email: '郵箱',
removeFromTeamTip: '將取消團隊訪問', removeFromTeamTip: '將取消團隊訪問',
setAdmin: '設為管理員', setAdmin: '設為管理員',
setMember: '設為普通成員', setMember: '設為普通成員',
setEditor: '設為編輯',
disinvite: '取消邀請', disinvite: '取消邀請',
deleteMember: '刪除成員', deleteMember: '刪除成員',
you: '(你)', you: '(你)',

+ 2
- 2
web/models/common.ts 查看文件

export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'created_at'> & { export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'created_at'> & {
avatar: string avatar: string
status: 'pending' | 'active' | 'banned' | 'closed' status: 'pending' | 'active' | 'banned' | 'closed'
role: 'owner' | 'admin' | 'normal'
role: 'owner' | 'admin' | 'editor' | 'normal'
} }


export enum ProviderName { export enum ProviderName {
} }


export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & { export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
role: 'normal' | 'admin' | 'owner'
role: 'owner' | 'admin' | 'editor' | 'normal'
providers: Provider[] providers: Provider[]
in_trail: boolean in_trail: boolean
trial_end_reason?: string trial_end_reason?: string

+ 2
- 2
web/utils/app-redirection.ts 查看文件

export const getRedirection = ( export const getRedirection = (
isCurrentWorkspaceManager: boolean,
isCurrentWorkspaceEditor: boolean,
app: any, app: any,
redirectionFunc: (href: string) => void, redirectionFunc: (href: string) => void,
) => { ) => {
if (!isCurrentWorkspaceManager) {
if (!isCurrentWorkspaceEditor) {
redirectionFunc(`/app/${app.id}/overview`) redirectionFunc(`/app/${app.id}/overview`)
} }
else { else {

Loading…
取消
儲存