| return {"id": upload_file.id}, 201 | 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(TenantListApi, "/workspaces") # GET for getting all tenants | ||||
| api.add_resource(WorkspaceListApi, "/all-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 | api.add_resource(TenantApi, "/workspaces/current", endpoint="workspaces_current") # GET for getting current tenant info | ||||
| api.add_resource(SwitchWorkspaceApi, "/workspaces/switch") # POST for switching tenant | api.add_resource(SwitchWorkspaceApi, "/workspaces/switch") # POST for switching tenant | ||||
| api.add_resource(CustomConfigWorkspaceApi, "/workspaces/custom-config") | api.add_resource(CustomConfigWorkspaceApi, "/workspaces/custom-config") | ||||
| api.add_resource(WebappLogoWorkspaceApi, "/workspaces/custom-config/webapp-logo/upload") | api.add_resource(WebappLogoWorkspaceApi, "/workspaces/custom-config/webapp-logo/upload") | ||||
| api.add_resource(WorkspaceInfoApi, "/workspaces/info") # POST for changing workspace info |
| '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 |
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import InviteModal from './invite-modal' | import InviteModal from './invite-modal' | ||||
| import InvitedModal from './invited-modal' | import InvitedModal from './invited-modal' | ||||
| import EditWorkspaceModal from './edit-workspace-modal' | |||||
| import Operation from './operation' | import Operation from './operation' | ||||
| import { fetchMembers } from '@/service/common' | import { fetchMembers } from '@/service/common' | ||||
| import I18n from '@/context/i18n' | import I18n from '@/context/i18n' | ||||
| import { NUM_INFINITE } from '@/app/components/billing/config' | import { NUM_INFINITE } from '@/app/components/billing/config' | ||||
| import { LanguagesSupported } from '@/i18n/language' | import { LanguagesSupported } from '@/i18n/language' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Tooltip from '@/app/components/base/tooltip' | |||||
| import { RiPencilLine } from '@remixicon/react' | |||||
| dayjs.extend(relativeTime) | dayjs.extend(relativeTime) | ||||
| const MembersPage = () => { | const MembersPage = () => { | ||||
| const { plan, enableBilling } = useProviderContext() | const { plan, enableBilling } = useProviderContext() | ||||
| const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise | const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise | ||||
| const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers | const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers | ||||
| const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <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> | <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> | ||||
| <div className='grow'> | <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> | </div> | ||||
| {isMemberFull && ( | {isMemberFull && ( | ||||
| /> | /> | ||||
| ) | ) | ||||
| } | } | ||||
| { | |||||
| editWorkspaceModalVisible && ( | |||||
| <EditWorkspaceModal | |||||
| onCancel={() => setEditWorkspaceModalVisible(false)} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </> | </> | ||||
| ) | ) | ||||
| } | } |
| feedbackTitle: 'Feedback', | feedbackTitle: 'Feedback', | ||||
| feedbackLabel: 'Tell us why you deleted your account?', | feedbackLabel: 'Tell us why you deleted your account?', | ||||
| feedbackPlaceholder: 'Optional', | feedbackPlaceholder: 'Optional', | ||||
| editWorkspaceInfo: 'Edit Workspace Info', | |||||
| workspaceName: 'Workspace Name', | |||||
| workspaceIcon: 'Workspace Icon', | |||||
| }, | }, | ||||
| members: { | members: { | ||||
| team: 'Team', | team: 'Team', |
| feedbackLabel: 'アカウントを削除した理由を教えてください。', | feedbackLabel: 'アカウントを削除した理由を教えてください。', | ||||
| feedbackPlaceholder: '随意', | feedbackPlaceholder: '随意', | ||||
| sendVerificationButton: '確認コードの送信', | sendVerificationButton: '確認コードの送信', | ||||
| editWorkspaceInfo: 'ワークスペース情報を編集', | |||||
| workspaceName: 'ワークスペース名', | |||||
| workspaceIcon: 'ワークスペースアイコン', | |||||
| }, | }, | ||||
| members: { | members: { | ||||
| team: 'チーム', | team: 'チーム', |
| feedbackTitle: '反馈', | feedbackTitle: '反馈', | ||||
| feedbackLabel: '请告诉我们您为什么删除账户?', | feedbackLabel: '请告诉我们您为什么删除账户?', | ||||
| feedbackPlaceholder: '选填', | feedbackPlaceholder: '选填', | ||||
| editWorkspaceInfo: '编辑工作空间信息', | |||||
| workspaceName: '工作空间名称', | |||||
| workspaceIcon: '工作空间图标', | |||||
| }, | }, | ||||
| members: { | members: { | ||||
| team: '团队', | team: '团队', |
| return post<CommonResponse & { new_tenant: IWorkspace }>(url, { body }) | 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 }) => { | export const fetchDataSource: Fetcher<{ data: DataSourceNotion[] }, { url: string }> = ({ url }) => { | ||||
| return get<{ data: DataSourceNotion[] }>(url) | return get<{ data: DataSourceNotion[] }>(url) | ||||
| } | } |