| @@ -216,6 +216,23 @@ class WebappLogoWorkspaceApi(Resource): | |||
| return {"id": upload_file.id}, 201 | |||
| class WorkspaceInfoApi(Resource): | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| # Change workspace name | |||
| def post(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("name", type=str, required=True, location="json") | |||
| args = parser.parse_args() | |||
| tenant = Tenant.query.filter(Tenant.id == current_user.current_tenant_id).one_or_404() | |||
| tenant.name = args["name"] | |||
| db.session.commit() | |||
| return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)} | |||
| api.add_resource(TenantListApi, "/workspaces") # GET for getting all tenants | |||
| api.add_resource(WorkspaceListApi, "/all-workspaces") # GET for getting all tenants | |||
| api.add_resource(TenantApi, "/workspaces/current", endpoint="workspaces_current") # GET for getting current tenant info | |||
| @@ -223,3 +240,4 @@ api.add_resource(TenantApi, "/info", endpoint="info") # Deprecated | |||
| api.add_resource(SwitchWorkspaceApi, "/workspaces/switch") # POST for switching tenant | |||
| api.add_resource(CustomConfigWorkspaceApi, "/workspaces/custom-config") | |||
| api.add_resource(WebappLogoWorkspaceApi, "/workspaces/custom-config/webapp-logo/upload") | |||
| api.add_resource(WorkspaceInfoApi, "/workspaces/info") # POST for changing workspace info | |||
| @@ -0,0 +1,87 @@ | |||
| 'use client' | |||
| import cn from '@/utils/classnames' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Input from '@/app/components/base/input' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useState } from 'react' | |||
| import { useContext } from 'use-context-selector' | |||
| import s from './index.module.css' | |||
| import Button from '@/app/components/base/button' | |||
| import { RiCloseLine } from '@remixicon/react' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { updateWorkspaceInfo } from '@/service/common' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| type IEditWorkspaceModalProps = { | |||
| onCancel: () => void | |||
| } | |||
| const EditWorkspaceModal = ({ | |||
| onCancel, | |||
| }: IEditWorkspaceModalProps) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useContext(ToastContext) | |||
| const { currentWorkspace, isCurrentWorkspaceOwner, mutateCurrentWorkspace } = useAppContext() | |||
| const [name, setName] = useState<string>(currentWorkspace.name) | |||
| const changeWorkspaceInfo = async (name: string) => { | |||
| try { | |||
| await updateWorkspaceInfo({ | |||
| url: '/workspaces/info', | |||
| body: { | |||
| name, | |||
| }, | |||
| }) | |||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | |||
| location.assign(`${location.origin}`) | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) | |||
| } | |||
| } | |||
| return ( | |||
| <div className={cn(s.wrap)}> | |||
| <Modal overflowVisible isShow onClose={() => {}} className={cn(s.modal)}> | |||
| <div className='mb-2 flex justify-between'> | |||
| <div className='text-xl font-semibold text-text-primary'>{t('common.account.editWorkspaceInfo')}</div> | |||
| <RiCloseLine className='h-4 w-4 cursor-pointer text-text-tertiary' onClick={onCancel} /> | |||
| </div> | |||
| <div> | |||
| <div className='mb-2 text-sm font-medium text-text-primary'>{t('common.account.workspaceName')}</div> | |||
| <Input | |||
| className='mb-2' | |||
| value={name} | |||
| placeholder={t('common.account.workspaceNamePlaceholder')} | |||
| onChange={(e) => { | |||
| setName(e.target.value) | |||
| }} | |||
| onClear={() => { | |||
| setName(currentWorkspace.name) | |||
| }} | |||
| /> | |||
| <div className='sticky bottom-0 -mx-2 mt-2 flex flex-wrap items-center justify-end gap-x-2 bg-components-panel-bg px-2 pt-4'> | |||
| <Button | |||
| size='large' | |||
| onClick={onCancel} | |||
| > | |||
| {t('common.operation.cancel')} | |||
| </Button> | |||
| <Button | |||
| size='large' | |||
| variant='primary' | |||
| onClick={() => { | |||
| changeWorkspaceInfo(name) | |||
| onCancel() | |||
| }} | |||
| disabled={!isCurrentWorkspaceOwner} | |||
| > | |||
| {t('common.operation.confirm')} | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| </Modal> | |||
| </div> | |||
| ) | |||
| } | |||
| export default EditWorkspaceModal | |||
| @@ -9,6 +9,7 @@ import { RiUserAddLine } from '@remixicon/react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import InviteModal from './invite-modal' | |||
| import InvitedModal from './invited-modal' | |||
| import EditWorkspaceModal from './edit-workspace-modal' | |||
| import Operation from './operation' | |||
| import { fetchMembers } from '@/service/common' | |||
| import I18n from '@/context/i18n' | |||
| @@ -22,6 +23,8 @@ import UpgradeBtn from '@/app/components/billing/upgrade-btn' | |||
| import { NUM_INFINITE } from '@/app/components/billing/config' | |||
| import { LanguagesSupported } from '@/i18n/language' | |||
| import cn from '@/utils/classnames' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import { RiPencilLine } from '@remixicon/react' | |||
| dayjs.extend(relativeTime) | |||
| const MembersPage = () => { | |||
| @@ -50,6 +53,7 @@ const MembersPage = () => { | |||
| const { plan, enableBilling } = useProviderContext() | |||
| const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise | |||
| const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers | |||
| const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false) | |||
| return ( | |||
| <> | |||
| @@ -59,26 +63,41 @@ const MembersPage = () => { | |||
| <span className='bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text font-semibold uppercase text-shadow-shadow-1 opacity-90'>{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span> | |||
| </div> | |||
| <div className='grow'> | |||
| <div className='system-md-semibold text-text-secondary'>{currentWorkspace?.name}</div> | |||
| {enableBilling && ( | |||
| <div className='system-xs-medium mt-1 text-text-tertiary'> | |||
| {isNotUnlimitedMemberPlan | |||
| ? ( | |||
| <div className='flex space-x-1'> | |||
| <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div> | |||
| <div className=''>{accounts.length}</div> | |||
| <div>/</div> | |||
| <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div> | |||
| </div> | |||
| ) | |||
| : ( | |||
| <div className='flex space-x-1'> | |||
| <div>{accounts.length}</div> | |||
| <div>{t('billing.plansCommon.memberAfter')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| )} | |||
| <div className='system-md-semibold flex items-center gap-1 text-text-secondary'> | |||
| <span>{currentWorkspace?.name}</span> | |||
| {isCurrentWorkspaceOwner && <span> | |||
| <Tooltip | |||
| popupContent={t('common.account.editWorkspaceInfo')} | |||
| needsDelay | |||
| > | |||
| <div | |||
| className='cursor-pointer rounded-md p-1 hover:bg-black/5' | |||
| onClick={() => { | |||
| setEditWorkspaceModalVisible(true) | |||
| }} | |||
| > | |||
| <RiPencilLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| </Tooltip> | |||
| </span>} | |||
| </div> | |||
| <div className='system-xs-medium mt-1 text-text-tertiary'> | |||
| {enableBilling && isNotUnlimitedMemberPlan | |||
| ? ( | |||
| <div className='flex space-x-1'> | |||
| <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div> | |||
| <div className=''>{accounts.length}</div> | |||
| <div>/</div> | |||
| <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div> | |||
| </div> | |||
| ) | |||
| : ( | |||
| <div className='flex space-x-1'> | |||
| <div>{accounts.length}</div> | |||
| <div>{t('billing.plansCommon.memberAfter')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {isMemberFull && ( | |||
| @@ -145,6 +164,13 @@ const MembersPage = () => { | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| editWorkspaceModalVisible && ( | |||
| <EditWorkspaceModal | |||
| onCancel={() => setEditWorkspaceModalVisible(false)} | |||
| /> | |||
| ) | |||
| } | |||
| </> | |||
| ) | |||
| } | |||
| @@ -218,6 +218,9 @@ const translation = { | |||
| feedbackTitle: 'Feedback', | |||
| feedbackLabel: 'Tell us why you deleted your account?', | |||
| feedbackPlaceholder: 'Optional', | |||
| editWorkspaceInfo: 'Edit Workspace Info', | |||
| workspaceName: 'Workspace Name', | |||
| workspaceIcon: 'Workspace Icon', | |||
| }, | |||
| members: { | |||
| team: 'Team', | |||
| @@ -218,6 +218,9 @@ const translation = { | |||
| feedbackLabel: 'アカウントを削除した理由を教えてください。', | |||
| feedbackPlaceholder: '随意', | |||
| sendVerificationButton: '確認コードの送信', | |||
| editWorkspaceInfo: 'ワークスペース情報を編集', | |||
| workspaceName: 'ワークスペース名', | |||
| workspaceIcon: 'ワークスペースアイコン', | |||
| }, | |||
| members: { | |||
| team: 'チーム', | |||
| @@ -218,6 +218,9 @@ const translation = { | |||
| feedbackTitle: '反馈', | |||
| feedbackLabel: '请告诉我们您为什么删除账户?', | |||
| feedbackPlaceholder: '选填', | |||
| editWorkspaceInfo: '编辑工作空间信息', | |||
| workspaceName: '工作空间名称', | |||
| workspaceIcon: '工作空间图标', | |||
| }, | |||
| members: { | |||
| team: '团队', | |||
| @@ -148,6 +148,10 @@ export const switchWorkspace: Fetcher<CommonResponse & { new_tenant: IWorkspace | |||
| return post<CommonResponse & { new_tenant: IWorkspace }>(url, { body }) | |||
| } | |||
| export const updateWorkspaceInfo: Fetcher<ICurrentWorkspace, { url: string; body: Record<string, any> }> = ({ url, body }) => { | |||
| return post<ICurrentWorkspace>(url, { body }) | |||
| } | |||
| export const fetchDataSource: Fetcher<{ data: DataSourceNotion[] }, { url: string }> = ({ url }) => { | |||
| return get<{ data: DataSourceNotion[] }>(url) | |||
| } | |||