Commit 4814d6ea authored by StyleZhang's avatar StyleZhang

feat: add notion-page-selector

parent 62a5a1ea
import cn from 'classnames'
type CheckboxProps = {
checked?: boolean
onCheck?: () => void
className?: string
}
const Checkbox = ({ checked, onCheck, className }: CheckboxProps) => {
return (
<div
className={cn('w-4 h-4 border rounded', checked ? 'border-primary-600 bg-primary-600' : 'border-gray-300', className)}
onClick={onCheck}
/>
)
}
export default Checkbox
.icon { .default-page-icon {
background: center center no-repeat; width: 20px;
height: 20px;
background: url(../notion-page-selector/assets/notion-page.svg) center center no-repeat;
background-size: cover; background-size: cover;
} }
\ No newline at end of file
import cn from 'classnames' import cn from 'classnames'
import s from './index.module.css'
type NotionIconProps = { type NotionIconProps = {
type?: 'workspace' | 'page' type?: 'workspace' | 'page'
src: string src?: string | null
name: string name?: string | null
className?: string className?: string
} }
...@@ -29,12 +30,28 @@ const NotionIcon = ({ ...@@ -29,12 +30,28 @@ const NotionIcon = ({
) )
} }
return ( return (
<div className={cn('flex items-center justify-center w-5 h-5 bg-gray-200 text-xs font-medium text-gray-500 rounded', className)}>{name[0].toLocaleUpperCase()}</div> <div className={cn('flex items-center justify-center w-5 h-5 bg-gray-200 text-xs font-medium text-gray-500 rounded', className)}>{name?.[0].toLocaleUpperCase()}</div>
)
}
if (src) {
return (
<img
alt='workspace icon'
src={src}
className={cn('block object-cover w-5 h-5', className)}
/>
)
}
if (name) {
return (
<div className={cn('flex items-center justify-center w-5 h-5', className)}>{src}</div>
) )
} }
return ( return (
<div></div> <div className={cn(s['default-page-icon'], className)} />
) )
} }
......
import { useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import useSWR from 'swr'
import cn from 'classnames' import cn from 'classnames'
import s from './base.module.css' import s from './base.module.css'
import WorkspaceSelector from './workspace-selector' import WorkspaceSelector from './workspace-selector'
import SearchInput from './search-input' import SearchInput from './search-input'
import PageSelector from './page-selector'
import { fetchDataSource } from '@/service/common'
const NotionPageSelector = () => { const NotionPageSelector = () => {
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
const { data } = useSWR({ url: 'data-source/integrates' }, fetchDataSource)
const notionWorkspaces = data?.data.filter(item => item.provider === 'notion') || []
const firstWorkspace = notionWorkspaces[0]?.id
const [currentWorkspaceId, setCurrentWorkspaceId] = useState('')
const currentWorkspace = notionWorkspaces.find(workspace => workspace.id === currentWorkspaceId)
const handleSearchValueChange = (value: string) => { const handleSearchValueChange = (value: string) => {
setSearchValue(value) setSearchValue(value)
} }
const handleSelectWorkspace = useCallback((workspaceId: string) => {
setCurrentWorkspaceId(workspaceId)
}, [])
useEffect(() => {
setCurrentWorkspaceId(firstWorkspace)
}, [firstWorkspace])
return ( return (
<div className='bg-gray-25 border border-gray-200 rounded-xl'> <div className='bg-gray-25 border border-gray-200 rounded-xl'>
<div className='flex items-center pl-[10px] pr-2 h-11 bg-white'> <div className='flex items-center pl-[10px] pr-2 h-11 bg-white border-b border-b-gray-200 rounded-t-xl'>
<WorkspaceSelector /> <WorkspaceSelector
value={currentWorkspaceId}
items={notionWorkspaces}
onSelect={handleSelectWorkspace}
/>
<div className='mx-1 w-[1px] h-3 bg-gray-200' /> <div className='mx-1 w-[1px] h-3 bg-gray-200' />
<div className={cn(s['setting-icon'], 'w-6 h-6 cursor-pointer')} /> <div className={cn(s['setting-icon'], 'w-6 h-6 cursor-pointer')} />
<div className='grow' /> <div className='grow' />
...@@ -21,12 +41,8 @@ const NotionPageSelector = () => { ...@@ -21,12 +41,8 @@ const NotionPageSelector = () => {
onChange={handleSearchValueChange} onChange={handleSearchValueChange}
/> />
</div> </div>
<div className='p-2'> <div className='rounded-b-xl overflow-hidden'>
<div className='flex items-center px-2 h-7 rounded-md'> <PageSelector list={currentWorkspace?.source_info.pages || []} />
<div className='mr-3 w-4 h-4'></div>
<div className='mr-2 w-5 h-5'></div>
<div className='text-sm font-medium text-gray-700'>sdfsfsfsd</div>
</div>
</div> </div>
</div> </div>
) )
......
.modal {
width: 600px !important;
max-width: 600px !important;
padding: 24px 32px !important;
}
.operate {
padding: 0 8px;
min-width: 96px;
height: 36px;
line-height: 36px;
text-align: center;
background-color: #ffffff;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 8px;
border: 0.5px solid #eaecf0;
font-size: 14px;
font-weight: 500;
color: #667085;
cursor: pointer;
}
.operate-save {
margin-left: 8px;
border-color: #155eef;
background-color: #155eef;
color: #ffffff;
}
\ No newline at end of file
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { XMarkIcon } from '@heroicons/react/24/outline'
import NotionPageSelector from '../base' import NotionPageSelector from '../base'
import s from './index.module.css'
import Modal from '@/app/components/base/modal'
type NotionPageSelectorModalProps = {
isShow: boolean
onClose: () => void
}
const NotionPageSelectorModal = ({
isShow,
onClose,
}: NotionPageSelectorModalProps) => {
const { t } = useTranslation()
const handleClose = () => {
onClose()
}
const NotionPageSelectorModal = () => {
return ( return (
<NotionPageSelector /> <Modal
className={s.modal}
isShow={isShow}
onClose={() => {}}
>
<div className='flex items-center justify-between mb-6 h-8'>
<div className='text-xl font-semibold text-gray-900'>{t('common.dataSource.notion.selector.addPages')}</div>
<div
className='flex items-center justify-center -mr-2 w-8 h-8 cursor-pointer'
onClick={handleClose}>
<XMarkIcon className='w-4 h-4' />
</div>
</div>
<NotionPageSelector />
<div className='mt-8 flex justify-end'>
<div className={s.operate} onClick={handleClose}>{t('common.operation.cancel')}</div>
<div className={cn(s.operate, s['operate-save'])}>{t('common.operation.save')}</div>
</div>
</Modal>
) )
} }
......
.arrow {
width: 20px;
height: 20px;
background: url(../assets/down-arrow.svg) center center no-repeat;
background-size: 16px 16px;
}
.arrow-collapse {
transform: rotate(-90deg);
}
\ No newline at end of file
import { memo } from 'react'
import { FixedSizeList as List, areEqual } from 'react-window'
import type { ListChildComponentProps } from 'react-window'
import cn from 'classnames'
import Checkbox from '../../checkbox'
import NotionIcon from '../../notion-icon'
import s from './index.module.css'
import type { DataSourceNotionPage } from '@/models/common'
const Item = memo(({ index, style, data }: ListChildComponentProps<{ list: DataSourceNotionPage[] }>) => {
const current = data.list[index]
let src, name
if (current.page_icon) {
try {
const icon = JSON.parse(current.page_icon)
if (icon?.type === 'emoji')
name = icon?.emoji
if (icon?.type === 'external')
src = icon?.external?.url
}
catch (e: any) {}
}
return (
<div
className='group flex items-center px-2 rounded-md hover:bg-gray-100 cursor-pointer'
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
>
<Checkbox className='shrink-0 mr-2 group-hover:border-primary-600 group-hover:border-[2px]' />
<div className={cn(s.arrow, s['arrow-collapse'], 'shrink-0 mr-1 w-5 h-5 hover:bg-gray-200 rounded-md')} />
<NotionIcon
className='shrink-0 mr-1'
type='page'
src={src}
name={name}
/>
<div
className='text-sm font-medium text-gray-700 truncate'
title={current.page_name}
>
{current.page_name}
</div>
</div>
)
}, areEqual)
type PageSelectorProps = {
list: DataSourceNotionPage[]
}
const PageSelector = ({
list,
}: PageSelectorProps) => {
return (
<List
className='py-2'
height={296}
itemCount={list.length}
itemSize={28}
width='100%'
itemData={{ list }}
>
{Item}
</List>
)
}
export default PageSelector
...@@ -5,4 +5,5 @@ ...@@ -5,4 +5,5 @@
.popup { .popup {
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
z-index: 10;
} }
\ No newline at end of file
...@@ -3,10 +3,22 @@ import { useTranslation } from 'react-i18next' ...@@ -3,10 +3,22 @@ import { useTranslation } from 'react-i18next'
import { Fragment } from 'react' import { Fragment } from 'react'
import { Menu, Transition } from '@headlessui/react' import { Menu, Transition } from '@headlessui/react'
import cn from 'classnames' import cn from 'classnames'
import NotionIcon from '../../notion-icon'
import s from './index.module.css' import s from './index.module.css'
import type { DataSourceNotion } from '@/models/common'
export default function WorkspaceSelector() { type WorkspaceSelectorProps = {
value: string
items: DataSourceNotion[]
onSelect: (v: string) => void
}
export default function WorkspaceSelector({
value,
items,
onSelect,
}: WorkspaceSelectorProps) {
const { t } = useTranslation() const { t } = useTranslation()
const currentWorkspace = items.find(item => item.id === value)?.source_info
return ( return (
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
...@@ -14,9 +26,13 @@ export default function WorkspaceSelector() { ...@@ -14,9 +26,13 @@ export default function WorkspaceSelector() {
({ open }) => ( ({ open }) => (
<> <>
<Menu.Button className={`flex items-center justify-center h-7 rounded-md hover:bg-gray-50 ${open && 'bg-gray-50'} cursor-pointer`}> <Menu.Button className={`flex items-center justify-center h-7 rounded-md hover:bg-gray-50 ${open && 'bg-gray-50'} cursor-pointer`}>
<div className='ml-1 mr-2 w-5 h-5 rounded'></div> <NotionIcon
<div className='mr-1 w-[90px] truncate'>Stylezhang's workspace</div> className='ml-1 mr-2'
<div className='mr-1 w-5 h-[18px] bg-primary-50 rounded-lg text-xs font-medium text-primary-600'>4</div> src={currentWorkspace?.workspace_icon}
name={currentWorkspace?.workspace_name}
/>
<div className='mr-1 w-[90px] text-left text-sm font-medium text-gray-700 truncate' title={currentWorkspace?.workspace_name}>{currentWorkspace?.workspace_name}</div>
<div className='mr-1 w-5 h-[18px] bg-primary-50 rounded-lg text-xs font-medium text-primary-600'>{currentWorkspace?.total}</div>
<div className={cn(s['down-arrow'], 'mr-2 w-3 h-3')} /> <div className={cn(s['down-arrow'], 'mr-2 w-3 h-3')} />
</Menu.Button> </Menu.Button>
<Transition <Transition
...@@ -36,25 +52,27 @@ export default function WorkspaceSelector() { ...@@ -36,25 +52,27 @@ export default function WorkspaceSelector() {
border-[0.5px] border-gray-200`, border-[0.5px] border-gray-200`,
)} )}
> >
<div className="p-1"> <div className="p-1 max-h-50 overflow-auto">
<Menu.Item> {
<div className='flex items-center px-3 h-9 hover:bg-gray-50 cursor-pointer'> items.map(item => (
<div className='mr-2 w-5 h-5 rounded'></div> <Menu.Item key={item.id}>
<div className='grow mr-2 text-sm text-gray-700'>LangGenius</div> <div
<div className='text-xs font-medium text-primary-600'> className='flex items-center px-3 h-9 hover:bg-gray-50 cursor-pointer'
{4} {t('common.dataSource.notion.selector.pageSelected')} onClick={() => onSelect(item.id)}
</div> >
</div> <NotionIcon
</Menu.Item> className='shrink-0 mr-2'
<Menu.Item> src={item.source_info.workspace_icon}
<div className='flex items-center px-3 h-9 hover:bg-gray-50 cursor-pointer'> name={item.source_info.workspace_name}
<div className='mr-2 w-5 h-5 rounded'></div> />
<div className='grow mr-2 text-sm text-gray-700'>LangGenius</div> <div className='grow mr-2 text-sm text-gray-700 truncate' title={item.source_info.workspace_name}>{item.source_info.workspace_name}</div>
<div className='text-xs font-medium text-primary-600'> <div className='shrink-0 text-xs font-medium text-primary-600'>
{4} {t('common.dataSource.notion.selector.pageSelected')} {item.source_info.total} {t('common.dataSource.notion.selector.pageSelected')}
</div> </div>
</div> </div>
</Menu.Item> </Menu.Item>
))
}
</div> </div>
</Menu.Items> </Menu.Items>
</Transition> </Transition>
......
...@@ -47,6 +47,7 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan ...@@ -47,6 +47,7 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
const selectedSegment = useSelectedLayoutSegment() const selectedSegment = useSelectedLayoutSegment()
const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon' const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon'
const isExplore = selectedSegment === 'explore' const isExplore = selectedSegment === 'explore'
return ( return (
<div className={classNames( <div className={classNames(
'sticky top-0 left-0 right-0 z-20 flex bg-gray-100 grow-0 shrink-0 basis-auto h-14', 'sticky top-0 left-0 right-0 z-20 flex bg-gray-100 grow-0 shrink-0 basis-auto h-14',
......
...@@ -100,19 +100,23 @@ export type IWorkspace = { ...@@ -100,19 +100,23 @@ export type IWorkspace = {
current: boolean current: boolean
} }
export type DataSourceNotionPage = {
page_icon: string | null
page_id: string
page_name: string
}
export type DataSourceNotionWorkspace = {
workspace_name: string
workspace_id: string
workspace_icon: string | null
total: number
pages: DataSourceNotionPage[]
}
export type DataSourceNotion = { export type DataSourceNotion = {
id: string id: string
provider: string provider: string
is_bound: boolean is_bound: boolean
source_info: { source_info: DataSourceNotionWorkspace
workspace_name: string
workspace_id: string
workspace_icon: string | null
total: number
pages: {
page_icon: string | null
page_id: string
page_name: string
}[]
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment