mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 13:15:51 +08:00
refactor!: remove next.js (#3267)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type React from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export type UploadProps = React.PropsWithChildren<{
|
||||
uploadType?: string;
|
||||
accept?: string;
|
||||
fileChange: (file: File) => void;
|
||||
}>;
|
||||
|
||||
export const Upload: React.FC<UploadProps> = ({
|
||||
fileChange,
|
||||
accept,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const input_ref = useRef<HTMLInputElement>(null);
|
||||
const _chooseFile = () => {
|
||||
if (input_ref.current) {
|
||||
input_ref.current.click();
|
||||
}
|
||||
};
|
||||
const _handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
fileChange(file);
|
||||
if (input_ref.current) {
|
||||
input_ref.current.value = '';
|
||||
}
|
||||
};
|
||||
return (
|
||||
<UploadStyle onClick={_chooseFile}>
|
||||
{children ?? <Button>{t['Upload']()}</Button>}
|
||||
<input
|
||||
ref={input_ref}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
onChange={_handleInputChange}
|
||||
accept={accept}
|
||||
{...props}
|
||||
/>
|
||||
</UploadStyle>
|
||||
);
|
||||
};
|
||||
|
||||
const UploadStyle = styled('div')(() => {
|
||||
return {
|
||||
display: 'inline-block',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type CSSProperties, type FC, forwardRef } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
|
||||
export const Footer: FC = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
return (
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
noBorder
|
||||
bold
|
||||
icon={
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
size: number;
|
||||
name: string | undefined;
|
||||
avatar: string | undefined;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
|
||||
function WorkspaceAvatar(props, ref) {
|
||||
const size = props.size || 20;
|
||||
const sizeStr = size + 'px';
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.avatar ? (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<picture>
|
||||
<img
|
||||
style={{ width: sizeStr, height: sizeStr }}
|
||||
src={props.avatar}
|
||||
alt=""
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...props.style,
|
||||
width: sizeStr,
|
||||
height: sizeStr,
|
||||
border: '1px solid #fff',
|
||||
color: '#fff',
|
||||
fontSize: Math.ceil(0.5 * size) + 'px',
|
||||
background: stringToColour(props.name || 'AFFiNE'),
|
||||
borderRadius: '50%',
|
||||
textAlign: 'center',
|
||||
lineHeight: size + 'px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
{(props.name || 'AFFiNE').substring(0, 1)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
displayFlex,
|
||||
displayInlineFlex,
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
width: '202px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
span: {
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '10px',
|
||||
maxWidth: '200px',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledFooter = styled('div')({
|
||||
padding: '20px 40px',
|
||||
flexShrink: 0,
|
||||
...displayFlex('space-between', 'center'),
|
||||
});
|
||||
|
||||
export const StyleUserInfo = styled('div')(() => {
|
||||
return {
|
||||
textAlign: 'left',
|
||||
marginLeft: '16px',
|
||||
flex: 1,
|
||||
p: {
|
||||
lineHeight: '24px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
},
|
||||
'p:first-of-type': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: 600,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeaderLeft = styled('div')(() => {
|
||||
return { ...displayFlex('flex-start', 'center') };
|
||||
});
|
||||
export const StyledModalTitle = styled('div')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledHelperContainer = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginLeft: '15px',
|
||||
fontWeight: 400,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalContent = styled('div')({
|
||||
height: '534px',
|
||||
padding: '8px 40px',
|
||||
marginTop: '72px',
|
||||
overflow: 'auto',
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
export const StyledOperationWrapper = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('flex-end', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceAdd = styled('div')(() => {
|
||||
return {
|
||||
width: '58px',
|
||||
height: '58px',
|
||||
borderRadius: '100%',
|
||||
background: '#f4f5fa',
|
||||
border: '1.5px dashed #f4f5fa',
|
||||
transition: 'background .2s',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '72px',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '0 40px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInButton = styled(Button)(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
paddingLeft: 0,
|
||||
'.circle': {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '20px',
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-primary-color)',
|
||||
fontSize: '24px',
|
||||
flexShrink: 0,
|
||||
marginRight: '16px',
|
||||
...displayInlineFlex('center', 'center'),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
export const HelpIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.0041 3.8002C7.47536 3.8002 3.8041 7.47146 3.8041 12.0002C3.8041 13.311 4.1111 14.5479 4.65639 15.6449C4.92133 16.1779 4.91348 16.8471 4.85228 17.3998C4.7869 17.9901 4.63749 18.6238 4.47572 19.1908C4.3722 19.5537 4.26086 19.8987 4.1573 20.2002H12.0041C16.5328 20.2002 20.2041 16.5289 20.2041 12.0002C20.2041 7.47146 16.5328 3.8002 12.0041 3.8002ZM2.26631 20.6926L2.2668 20.6914L2.26948 20.685L2.28104 20.6567C2.29139 20.6312 2.30687 20.5928 2.32657 20.5429C2.36599 20.4431 2.42218 20.2979 2.48795 20.1192C2.61992 19.7607 2.78833 19.2734 2.93712 18.7519C3.08715 18.226 3.21054 17.6884 3.262 17.2236C3.31765 16.7212 3.2707 16.4518 3.22364 16.3571C2.57081 15.0438 2.2041 13.5637 2.2041 12.0002C2.2041 6.58781 6.59171 2.2002 12.0041 2.2002C17.4165 2.2002 21.8041 6.5878 21.8041 12.0002C21.8041 17.4126 17.4165 21.8002 12.0041 21.8002H3.0049C2.73745 21.8002 2.4876 21.6665 2.33922 21.444C2.19087 21.2215 2.16356 20.9395 2.26631 20.6926ZM11.9672 9.0502C11.4091 9.0502 10.9382 9.43186 10.8049 9.9496C10.6948 10.3775 10.2587 10.6351 9.83079 10.5249C9.40291 10.4148 9.14532 9.97867 9.25545 9.55079C9.56623 8.3433 10.6614 7.4502 11.9672 7.4502C13.5136 7.4502 14.7672 8.7038 14.7672 10.2502C14.7672 11.1058 14.3536 11.6751 13.8978 12.115C13.7108 12.2955 13.4978 12.4721 13.2997 12.6362C13.2705 12.6604 13.2416 12.6844 13.2132 12.7081C12.982 12.9004 12.7556 13.0932 12.5329 13.3159C12.2205 13.6283 11.7139 13.6283 11.4015 13.3159C11.0891 13.0035 11.0891 12.4969 11.4015 12.1845C11.6788 11.9072 11.9525 11.6756 12.19 11.478C12.2213 11.4519 12.2517 11.4267 12.2812 11.4022C12.4849 11.2332 12.6465 11.0991 12.7866 10.9638C13.0808 10.6799 13.1672 10.4992 13.1672 10.2502C13.1672 9.58745 12.6299 9.0502 11.9672 9.0502ZM11.9772 16.5502H11.9672C11.5254 16.5502 11.1672 16.192 11.1672 15.7502C11.1672 15.3084 11.5254 14.9502 11.9672 14.9502H11.9772C12.419 14.9502 12.7772 15.3084 12.7772 15.7502C12.7772 16.192 12.419 16.5502 11.9772 16.5502Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ContactIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.16957 3.25H14.8304C15.3646 3.24999 15.8104 3.24998 16.1747 3.27974C16.5546 3.31078 16.9112 3.37789 17.2485 3.54973C17.7659 3.81338 18.1866 4.23408 18.4503 4.75153C18.6221 5.08879 18.6892 5.44545 18.7203 5.82533C18.75 6.18956 18.75 6.6354 18.75 7.16955V8.36732C18.9209 8.40912 19.0869 8.46741 19.2485 8.54973C19.7659 8.81339 20.1866 9.23408 20.4503 9.75153C20.6221 10.0888 20.6892 10.4454 20.7203 10.8253C20.75 11.1896 20.75 11.6354 20.75 12.1695V20C20.75 20.2766 20.5978 20.5307 20.3539 20.6613C20.11 20.7918 19.8141 20.7775 19.584 20.624L17.3901 19.1615C17.069 18.9474 16.9819 18.8925 16.8945 18.8544C16.8046 18.8151 16.7104 18.7866 16.6138 18.7694C16.52 18.7527 16.4171 18.75 16.0311 18.75H11.1695C10.6354 18.75 10.1896 18.75 9.82533 18.7203C9.44545 18.6892 9.08879 18.6221 8.75153 18.4503C8.23408 18.1866 7.81339 17.7659 7.54973 17.2485C7.52446 17.1989 7.50144 17.1488 7.48046 17.0983L4.33541 18.6708C4.10292 18.7871 3.82681 18.7746 3.6057 18.638C3.38459 18.5013 3.25 18.2599 3.25 18L3.25 7.16957C3.24999 6.63541 3.24998 6.18956 3.27974 5.82533C3.31078 5.44545 3.37789 5.08879 3.54973 4.75153C3.81338 4.23408 4.23408 3.81338 4.75153 3.54973C5.08879 3.37789 5.44545 3.31078 5.82533 3.27974C6.18956 3.24998 6.63541 3.24999 7.16957 3.25ZM7.25322 15.5349C7.25 15.318 7.25 15.0803 7.25 14.8211L7.25 12.1696C7.24999 11.6354 7.24998 11.1896 7.27974 10.8253C7.31078 10.4454 7.37789 10.0888 7.54973 9.75153C7.81339 9.23408 8.23408 8.81339 8.75153 8.54973C9.08879 8.37789 9.44545 8.31078 9.82533 8.27974C10.1896 8.24998 10.6354 8.24999 11.1696 8.25H16.8304C16.9769 8.25 17.1167 8.24999 17.25 8.25061V7.2C17.25 6.62757 17.2494 6.24336 17.2252 5.94748C17.2018 5.66036 17.1599 5.52307 17.1138 5.43251C16.9939 5.19731 16.8027 5.00608 16.5675 4.88624C16.4769 4.8401 16.3396 4.79822 16.0525 4.77476C15.7566 4.75058 15.3724 4.75 14.8 4.75H7.2C6.62757 4.75 6.24336 4.75058 5.94748 4.77476C5.66036 4.79822 5.52307 4.8401 5.43251 4.88624C5.19731 5.00608 5.00608 5.19731 4.88624 5.43251C4.8401 5.52307 4.79822 5.66036 4.77476 5.94748C4.75058 6.24336 4.75 6.62757 4.75 7.2V16.7865L7.25322 15.5349ZM9.94748 9.77476C9.66036 9.79822 9.52307 9.8401 9.43251 9.88624C9.19731 10.0061 9.00608 10.1973 8.88624 10.4325C8.8401 10.5231 8.79822 10.6604 8.77476 10.9475C8.75058 11.2434 8.75 11.6276 8.75 12.2V14.8C8.75 15.3018 8.7503 15.6608 8.7673 15.9461C8.78877 16.3064 8.83405 16.465 8.88624 16.5675C9.00608 16.8027 9.19731 16.9939 9.43251 17.1138C9.52307 17.1599 9.66036 17.2018 9.94748 17.2252C10.2434 17.2494 10.6276 17.25 11.2 17.25H16.0311C16.049 17.25 16.0666 17.25 16.084 17.25C16.3929 17.2499 16.6362 17.2497 16.877 17.2927C17.0895 17.3306 17.2968 17.3933 17.4947 17.4797C17.7189 17.5775 17.9213 17.7126 18.1782 17.884C18.1927 17.8937 18.2073 17.9035 18.2222 17.9134L19.25 18.5986V12.2C19.25 11.6276 19.2494 11.2434 19.2252 10.9475C19.2018 10.6604 19.1599 10.5231 19.1138 10.4325C18.9939 10.1973 18.8027 10.0061 18.5675 9.88624C18.4769 9.8401 18.3396 9.79822 18.0525 9.77476C17.7566 9.75058 17.3724 9.75 16.8 9.75H11.2C10.6276 9.75 10.2434 9.75058 9.94748 9.77476Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyboardIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M19.745 5C20.3417 5 20.914 5.23705 21.336 5.65901C21.7579 6.08097 21.995 6.65326 21.995 7.25V16.755C21.995 17.3517 21.7579 17.924 21.336 18.346C20.914 18.7679 20.3417 19.005 19.745 19.005H4.25C3.95453 19.005 3.66194 18.9468 3.38896 18.8337C3.11598 18.7207 2.86794 18.5549 2.65901 18.346C2.45008 18.1371 2.28434 17.889 2.17127 17.616C2.0582 17.3431 2 17.0505 2 16.755V7.25C2 6.65326 2.23705 6.08097 2.65901 5.65901C3.08097 5.23705 3.65326 5 4.25 5H19.745ZM19.745 6.5H4.25C4.05109 6.5 3.86032 6.57902 3.71967 6.71967C3.57902 6.86032 3.5 7.05109 3.5 7.25V16.755C3.5 17.169 3.836 17.505 4.25 17.505H19.745C19.9439 17.505 20.1347 17.426 20.2753 17.2853C20.416 17.1447 20.495 16.9539 20.495 16.755V7.25C20.495 7.05109 20.416 6.86032 20.2753 6.71967C20.1347 6.57902 19.9439 6.5 19.745 6.5ZM6.75 14.5H17.25C17.44 14.5001 17.6229 14.5722 17.7618 14.702C17.9006 14.8317 17.9851 15.0093 17.998 15.1989C18.011 15.3885 17.9515 15.5759 17.8316 15.7233C17.7117 15.8707 17.5402 15.9671 17.352 15.993L17.25 16H6.75C6.55998 15.9999 6.37706 15.9278 6.23821 15.798C6.09936 15.6683 6.01493 15.4907 6.00197 15.3011C5.98902 15.1115 6.04852 14.9241 6.16843 14.7767C6.28835 14.6293 6.45975 14.5329 6.648 14.507L6.75 14.5H17.25H6.75ZM16.5 11C16.7652 11 17.0196 11.1054 17.2071 11.2929C17.3946 11.4804 17.5 11.7348 17.5 12C17.5 12.2652 17.3946 12.5196 17.2071 12.7071C17.0196 12.8946 16.7652 13 16.5 13C16.2348 13 15.9804 12.8946 15.7929 12.7071C15.6054 12.5196 15.5 12.2652 15.5 12C15.5 11.7348 15.6054 11.4804 15.7929 11.2929C15.9804 11.1054 16.2348 11 16.5 11ZM10.505 11C10.7702 11 11.0246 11.1054 11.2121 11.2929C11.3996 11.4804 11.505 11.7348 11.505 12C11.505 12.2652 11.3996 12.5196 11.2121 12.7071C11.0246 12.8946 10.7702 13 10.505 13C10.2398 13 9.98543 12.8946 9.79789 12.7071C9.61036 12.5196 9.505 12.2652 9.505 12C9.505 11.7348 9.61036 11.4804 9.79789 11.2929C9.98543 11.1054 10.2398 11 10.505 11ZM7.505 11C7.77022 11 8.02457 11.1054 8.21211 11.2929C8.39964 11.4804 8.505 11.7348 8.505 12C8.505 12.2652 8.39964 12.5196 8.21211 12.7071C8.02457 12.8946 7.77022 13 7.505 13C7.23978 13 6.98543 12.8946 6.79789 12.7071C6.61036 12.5196 6.505 12.2652 6.505 12C6.505 11.7348 6.61036 11.4804 6.79789 11.2929C6.98543 11.1054 7.23978 11 7.505 11ZM13.505 11C13.7702 11 14.0246 11.1054 14.2121 11.2929C14.3996 11.4804 14.505 11.7348 14.505 12C14.505 12.2652 14.3996 12.5196 14.2121 12.7071C14.0246 12.8946 13.7702 13 13.505 13C13.2398 13 12.9854 12.8946 12.7979 12.7071C12.6104 12.5196 12.505 12.2652 12.505 12C12.505 11.7348 12.6104 11.4804 12.7979 11.2929C12.9854 11.1054 13.2398 11 13.505 11ZM6 8C6.26522 8 6.51957 8.10536 6.70711 8.29289C6.89464 8.48043 7 8.73478 7 9C7 9.26522 6.89464 9.51957 6.70711 9.70711C6.51957 9.89464 6.26522 10 6 10C5.73478 10 5.48043 9.89464 5.29289 9.70711C5.10536 9.51957 5 9.26522 5 9C5 8.73478 5.10536 8.48043 5.29289 8.29289C5.48043 8.10536 5.73478 8 6 8ZM8.995 8C9.26022 8 9.51457 8.10536 9.70211 8.29289C9.88964 8.48043 9.995 8.73478 9.995 9C9.995 9.26522 9.88964 9.51957 9.70211 9.70711C9.51457 9.89464 9.26022 10 8.995 10C8.72978 10 8.47543 9.89464 8.28789 9.70711C8.10036 9.51957 7.995 9.26522 7.995 9C7.995 8.73478 8.10036 8.48043 8.28789 8.29289C8.47543 8.10536 8.72978 8 8.995 8ZM11.995 8C12.2602 8 12.5146 8.10536 12.7021 8.29289C12.8896 8.48043 12.995 8.73478 12.995 9C12.995 9.26522 12.8896 9.51957 12.7021 9.70711C12.5146 9.89464 12.2602 10 11.995 10C11.7298 10 11.4754 9.89464 11.2879 9.70711C11.1004 9.51957 10.995 9.26522 10.995 9C10.995 8.73478 11.1004 8.48043 11.2879 8.29289C11.4754 8.10536 11.7298 8 11.995 8ZM14.995 8C15.2602 8 15.5146 8.10536 15.7021 8.29289C15.8896 8.48043 15.995 8.73478 15.995 9C15.995 9.26522 15.8896 9.51957 15.7021 9.70711C15.5146 9.89464 15.2602 10 14.995 10C14.7298 10 14.4754 9.89464 14.2879 9.70711C14.1004 9.51957 13.995 9.26522 13.995 9C13.995 8.73478 14.1004 8.48043 14.2879 8.29289C14.4754 8.10536 14.7298 8 14.995 8ZM17.995 8C18.2602 8 18.5146 8.10536 18.7021 8.29289C18.8896 8.48043 18.995 8.73478 18.995 9C18.995 9.26522 18.8896 9.51957 18.7021 9.70711C18.5146 9.89464 18.2602 10 17.995 10C17.7298 10 17.4754 9.89464 17.2879 9.70711C17.1004 9.51957 16.995 9.26522 16.995 9C16.995 8.73478 17.1004 8.48043 17.2879 8.29289C17.4754 8.10536 17.7298 8 17.995 8Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { MuiFade, Tooltip } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon, UserGuideIcon } from '@blocksuite/icons';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { openOnboardingModalAtom, openSettingModalAtom } from '../../../atoms';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import { ShortcutsModal } from '../shortcuts-modal';
|
||||
import { ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import {
|
||||
StyledAnimateWrapper,
|
||||
StyledIconWrapper,
|
||||
StyledIsland,
|
||||
StyledTriggerWrapper,
|
||||
} from './style';
|
||||
|
||||
const DEFAULT_SHOW_LIST: IslandItemNames[] = [
|
||||
'whatNew',
|
||||
'contact',
|
||||
'shortcuts',
|
||||
];
|
||||
const DESKTOP_SHOW_LIST: IslandItemNames[] = [...DEFAULT_SHOW_LIST, 'guide'];
|
||||
export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts' | 'guide';
|
||||
export const HelpIsland = ({
|
||||
showList = environment.isDesktop ? DESKTOP_SHOW_LIST : DEFAULT_SHOW_LIST,
|
||||
}: {
|
||||
showList?: IslandItemNames[];
|
||||
}) => {
|
||||
const mode = useAtomValue(currentModeAtom);
|
||||
const setOpenOnboarding = useSetAtom(openOnboardingModalAtom);
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const [spread, setShowSpread] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [openShortCut, setOpenShortCut] = useState(false);
|
||||
|
||||
const openAbout = useCallback(() => {
|
||||
setShowSpread(false);
|
||||
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'about',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledIsland
|
||||
spread={spread}
|
||||
data-testid="help-island"
|
||||
onClick={() => {
|
||||
setShowSpread(!spread);
|
||||
}}
|
||||
inEdgelessPage={mode === 'edgeless'}
|
||||
>
|
||||
<StyledAnimateWrapper
|
||||
style={{ height: spread ? `${showList.length * 44}px` : 0 }}
|
||||
>
|
||||
{showList.includes('whatNew') && (
|
||||
<Tooltip content={t["Discover what's new!"]()} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-change-log-icon"
|
||||
onClick={() => {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<NewIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content={t['Contact Us']()} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
onClick={openAbout}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip content={t['Keyboard Shortcuts']()} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
setOpenShortCut(true);
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('guide') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.helpIsland.gettingStarted']()}
|
||||
placement="left-end"
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="easy-guide"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
setOpenOnboarding(true);
|
||||
}}
|
||||
>
|
||||
<UserGuideIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledAnimateWrapper>
|
||||
|
||||
<Tooltip content={t['Help and Feedback']()} placement="left-end">
|
||||
<MuiFade in={!spread} data-testid="faq-icon">
|
||||
<StyledTriggerWrapper>
|
||||
<HelpIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
</Tooltip>
|
||||
<MuiFade in={spread}>
|
||||
<StyledTriggerWrapper spread>
|
||||
<CloseIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
</StyledIsland>
|
||||
<ShortcutsModal
|
||||
open={openShortCut}
|
||||
onClose={() => setOpenShortCut(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { displayFlex, positionAbsolute, styled } from '@affine/component';
|
||||
|
||||
export const StyledIsland = styled('div')<{
|
||||
spread: boolean;
|
||||
inEdgelessPage?: boolean;
|
||||
}>(({ spread, inEdgelessPage }) => {
|
||||
return {
|
||||
transition: 'box-shadow 0.2s',
|
||||
width: '44px',
|
||||
position: 'relative',
|
||||
boxShadow: spread
|
||||
? 'var(--affine-menu-shadow)'
|
||||
: inEdgelessPage
|
||||
? 'var(--affine-menu-shadow)'
|
||||
: 'unset',
|
||||
padding: '0 4px 44px',
|
||||
borderRadius: '10px',
|
||||
background: spread
|
||||
? 'var(--affine-background-overlay-panel-color)'
|
||||
: 'var(--affine-background-primary-color)',
|
||||
':hover': {
|
||||
background: spread ? null : 'var(--affine-white)',
|
||||
boxShadow: spread ? null : 'var(--affine-menu-shadow)',
|
||||
},
|
||||
'::after': {
|
||||
content: '""',
|
||||
width: '36px',
|
||||
height: '1px',
|
||||
background: spread ? 'var(--affine-border-color)' : 'transparent',
|
||||
...positionAbsolute({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: '44px',
|
||||
}),
|
||||
margin: 'auto',
|
||||
transition: 'background 0.15s',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledIconWrapper = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
...displayFlex('center', 'center'),
|
||||
cursor: 'pointer',
|
||||
fontSize: '24px',
|
||||
borderRadius: '5px',
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
margin: '4px auto 4px',
|
||||
transition: 'background-color 0.2s',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledAnimateWrapper = styled('div')(() => ({
|
||||
transition: 'height 0.2s cubic-bezier(0, 0, 0.55, 1.6)',
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
export const StyledTriggerWrapper = styled('div')<{
|
||||
spread?: boolean;
|
||||
}>(({ spread }) => {
|
||||
return {
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-icon-color)',
|
||||
borderRadius: '5px',
|
||||
fontSize: '24px',
|
||||
...displayFlex('center', 'center'),
|
||||
...positionAbsolute({ left: '4px', bottom: '4px' }),
|
||||
':hover': {
|
||||
backgroundColor: spread ? 'var(--affine-hover-color)' : null,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
CloudWorkspaceIcon as DefaultCloudWorkspaceIcon,
|
||||
CollaborationIcon as DefaultJoinedWorkspaceIcon,
|
||||
LocalDataIcon as DefaultLocalDataIcon,
|
||||
LocalWorkspaceIcon as DefaultLocalWorkspaceIcon,
|
||||
PublishIcon as DefaultPublishIcon,
|
||||
} from '@blocksuite/icons';
|
||||
|
||||
// Here are some icons with special color or size
|
||||
|
||||
export const JoinedWorkspaceIcon = () => {
|
||||
return <DefaultJoinedWorkspaceIcon style={{ color: '#FF646B' }} />;
|
||||
};
|
||||
export const LocalWorkspaceIcon = () => {
|
||||
return <DefaultLocalWorkspaceIcon style={{ color: '#FDBD32' }} />;
|
||||
};
|
||||
|
||||
export const CloudWorkspaceIcon = () => {
|
||||
return <DefaultCloudWorkspaceIcon style={{ color: '#60A5FA' }} />;
|
||||
};
|
||||
|
||||
export const LocalDataIcon = () => {
|
||||
return <DefaultLocalDataIcon style={{ color: '#62CD80' }} />;
|
||||
};
|
||||
|
||||
export const PublishIcon = () => {
|
||||
return <DefaultPublishIcon style={{ color: '#8699FF' }} />;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { IconButtonProps } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
|
||||
const StyledIconButtonWithAnimate = styled(IconButton)(() => {
|
||||
return {
|
||||
svg: {
|
||||
transition: 'transform 0.15s ease-in-out',
|
||||
},
|
||||
':hover': {
|
||||
svg: {
|
||||
transform: 'translateY(3px)',
|
||||
},
|
||||
'::after': {
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// fixme(himself65): need to refactor
|
||||
export const QuickSearchButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
return (
|
||||
<StyledIconButtonWithAnimate
|
||||
data-testid="header-quickSearchButton"
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<ArrowDownSmallIcon />
|
||||
</StyledIconButtonWithAnimate>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { FC, SVGProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
import { pathGenerator } from '../../../shared';
|
||||
|
||||
export const useSwitchToConfig = (
|
||||
workspaceId: string
|
||||
): {
|
||||
title: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
}[] => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t['All pages'](),
|
||||
href: pathGenerator.all(workspaceId),
|
||||
icon: FolderIcon,
|
||||
},
|
||||
{
|
||||
title: t['Workspace Settings'](),
|
||||
onClick: () => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t['Trash'](),
|
||||
href: pathGenerator.trash(workspaceId),
|
||||
icon: DeleteTemporarilyIcon,
|
||||
},
|
||||
],
|
||||
[t, workspaceId, setOpenSettingModalAtom]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { Command } from 'cmdk';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { StyledModalFooterContent } from './style';
|
||||
|
||||
export type FooterProps = {
|
||||
query: string;
|
||||
onClose: () => void;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
};
|
||||
|
||||
export const Footer: React.FC<FooterProps> = ({
|
||||
query,
|
||||
onClose,
|
||||
blockSuiteWorkspace,
|
||||
}) => {
|
||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const MAX_QUERY_SHOW_LENGTH = 20;
|
||||
const normalizedQuery =
|
||||
query.length > MAX_QUERY_SHOW_LENGTH
|
||||
? query.slice(0, MAX_QUERY_SHOW_LENGTH) + '...'
|
||||
: query;
|
||||
return (
|
||||
<Command.Item
|
||||
data-testid="quick-search-add-new-page"
|
||||
onSelect={useCallback(async () => {
|
||||
const id = nanoid();
|
||||
const page = createPage(id);
|
||||
assertEquals(page.id, id);
|
||||
await initEmptyPage(page);
|
||||
const block = page.getBlockByFlavour(
|
||||
'affine:page'
|
||||
)[0] as PageBlockModel;
|
||||
if (block) {
|
||||
block.title.insert(query, 0);
|
||||
} else {
|
||||
console.warn('No page block found');
|
||||
}
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
title: query,
|
||||
});
|
||||
onClose();
|
||||
jumpToPage(blockSuiteWorkspace.id, page.id);
|
||||
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
|
||||
>
|
||||
<StyledModalFooterContent>
|
||||
<PlusIcon />
|
||||
{query ? (
|
||||
<span>{t['New Keyword Page']({ query: normalizedQuery })}</span>
|
||||
) : (
|
||||
<span>{t['New Page']()}</span>
|
||||
)}
|
||||
</StyledModalFooterContent>
|
||||
</Command.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Command } from 'cmdk';
|
||||
import { startTransition } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { Footer } from './footer';
|
||||
import { Results } from './results';
|
||||
import { SearchInput } from './search-input';
|
||||
import {
|
||||
StyledContent,
|
||||
StyledModalDivider,
|
||||
StyledModalFooter,
|
||||
StyledModalHeader,
|
||||
StyledShortcut,
|
||||
} from './style';
|
||||
|
||||
export type QuickSearchModalProps = {
|
||||
workspace: AllWorkspace;
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
open,
|
||||
setOpen,
|
||||
workspace,
|
||||
}) => {
|
||||
const blockSuiteWorkspace = workspace?.blockSuiteWorkspace;
|
||||
const t = useAFFiNEI18N();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [query, _setQuery] = useState('');
|
||||
const setQuery = useCallback((query: string) => {
|
||||
startTransition(() => {
|
||||
_setQuery(query);
|
||||
});
|
||||
}, []);
|
||||
const location = useLocation();
|
||||
const isPublicWorkspace = useMemo(
|
||||
() => location.pathname.startsWith('/public-workspace'),
|
||||
[location]
|
||||
);
|
||||
const [showCreatePage, setShowCreatePage] = useState(true);
|
||||
const isPublicAndNoQuery = useCallback(() => {
|
||||
return isPublicWorkspace && query.length === 0;
|
||||
}, [isPublicWorkspace, query.length]);
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
// Add ‘⌘+K’ shortcut keys as switches
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
|
||||
const selection = window.getSelection();
|
||||
// prevent search bar focus in firefox
|
||||
e.preventDefault();
|
||||
setQuery('');
|
||||
if (selection?.toString()) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
setOpen(!open);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [open, setOpen, setQuery]);
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Waiting for DOM rendering
|
||||
requestAnimationFrame(() => {
|
||||
const inputElement = inputRef.current;
|
||||
inputElement?.focus();
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
wrapperPosition={['top', 'center']}
|
||||
data-testid="quickSearch"
|
||||
>
|
||||
<ModalWrapper
|
||||
width={608}
|
||||
style={{
|
||||
maxHeight: '80vh',
|
||||
minHeight: isPublicAndNoQuery() ? '72px' : '412px',
|
||||
top: '80px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* <NavigationPath
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
onJumpToPage={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/> */}
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
//Handle KeyboardEvent conflicts with blocksuite
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowRight'
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
onValueChange={value => {
|
||||
setQuery(value);
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
// Avoid triggering the cmdk onSelect event when the input method is in use
|
||||
if (e.nativeEvent.isComposing) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
placeholder={t['Quick search placeholder']()}
|
||||
/>
|
||||
<StyledShortcut>
|
||||
{environment.isBrowser && environment.isMacOs
|
||||
? '⌘ + K'
|
||||
: 'Ctrl + K'}
|
||||
</StyledShortcut>
|
||||
</StyledModalHeader>
|
||||
<StyledModalDivider
|
||||
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
|
||||
/>
|
||||
<Command.List>
|
||||
<StyledContent
|
||||
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
|
||||
>
|
||||
<Results
|
||||
query={query}
|
||||
onClose={handleClose}
|
||||
workspace={workspace}
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
</StyledContent>
|
||||
{isPublicWorkspace ? null : showCreatePage ? (
|
||||
<>
|
||||
<StyledModalDivider />
|
||||
<StyledModalFooter>
|
||||
<Footer
|
||||
query={query}
|
||||
onClose={handleClose}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
</StyledModalFooter>
|
||||
</>
|
||||
) : null}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSearchModal;
|
||||
@@ -0,0 +1,147 @@
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { Command } from 'cmdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { recentPageSettingsAtom } from '../../../atoms';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { useSwitchToConfig } from './config';
|
||||
import { StyledListItem, StyledNotFound } from './style';
|
||||
|
||||
export type ResultsProps = {
|
||||
workspace: AllWorkspace;
|
||||
query: string;
|
||||
onClose: () => void;
|
||||
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
export const Results: FC<ResultsProps> = ({
|
||||
query,
|
||||
workspace,
|
||||
setShowCreatePage,
|
||||
onClose,
|
||||
}) => {
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
assertExists(blockSuiteWorkspace.id);
|
||||
const List = useSwitchToConfig(workspace.id);
|
||||
|
||||
const recentPageSetting = useAtomValue(recentPageSettingsAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
const navigate = useNavigate();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const results = blockSuiteWorkspace.search({ query });
|
||||
|
||||
// remove `space:` prefix
|
||||
const pageIds = [...results.values()].map(id => id.slice(6));
|
||||
|
||||
const resultsPageMeta = pageList.filter(
|
||||
page => pageIds.indexOf(page.id) > -1 && !page.trash
|
||||
);
|
||||
|
||||
const recentlyViewedItem = recentPageSetting.filter(recent => {
|
||||
const page = pageList.find(page => recent.id === page.id);
|
||||
if (!page) {
|
||||
return false;
|
||||
} else {
|
||||
return page.trash !== true;
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
setShowCreatePage(!resultsPageMeta.length);
|
||||
//Determine whether to display the ‘+ New page’
|
||||
}, [resultsPageMeta.length, setShowCreatePage]);
|
||||
if (!query) {
|
||||
return (
|
||||
<>
|
||||
{recentlyViewedItem.length > 0 && (
|
||||
<Command.Group heading={t['Recent']()}>
|
||||
{recentlyViewedItem.map(recent => {
|
||||
const page = pageList.find(page => recent.id === page.id);
|
||||
assertExists(page);
|
||||
return (
|
||||
<Command.Item
|
||||
key={page.id}
|
||||
value={page.id}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
jumpToPage(blockSuiteWorkspace.id, page.id);
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
{recent.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PageIcon />
|
||||
)}
|
||||
<span>{page.title || UNTITLED_WORKSPACE_NAME}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
)}
|
||||
<Command.Group heading={t['Jump to']()}>
|
||||
{List.map(link => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={link.title}
|
||||
value={link.title}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
link.href && navigate(link.href);
|
||||
link.onClick?.();
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
<link.icon />
|
||||
<span>{link.title}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!resultsPageMeta.length) {
|
||||
return (
|
||||
<StyledNotFound>
|
||||
<span>{t['Find 0 result']()}</span>
|
||||
<image href="/imgs/no-result.svg" width={200} height={200} />
|
||||
</StyledNotFound>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Command.Group
|
||||
heading={t['Find results']({ number: `${resultsPageMeta.length}` })}
|
||||
>
|
||||
{resultsPageMeta.map(result => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={result.id}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
assertExists(blockSuiteWorkspace.id);
|
||||
jumpToPage(blockSuiteWorkspace.id, result.id);
|
||||
}}
|
||||
value={result.id}
|
||||
>
|
||||
<StyledListItem>
|
||||
{result.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
||||
<span>{result.title || UNTITLED_WORKSPACE_NAME}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { Command } from 'cmdk';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { StyledInputContent, StyledLabel } from './style';
|
||||
|
||||
export const SearchInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'value' | 'onChange' | 'type'
|
||||
> & {
|
||||
/**
|
||||
* Optional controlled state for the value of the search input.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* Event handler called when the search value changes.
|
||||
*/
|
||||
onValueChange?: (search: string) => void;
|
||||
} & React.RefAttributes<HTMLInputElement>
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<StyledInputContent>
|
||||
<StyledLabel htmlFor=":r5:">
|
||||
<SearchIcon />
|
||||
</StyledLabel>
|
||||
<Command.Input ref={ref} {...props} />
|
||||
</StyledInputContent>
|
||||
);
|
||||
});
|
||||
|
||||
SearchInput.displayName = 'SearchInput';
|
||||
@@ -0,0 +1,180 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledContent = styled('div')(() => {
|
||||
return {
|
||||
minHeight: '290px',
|
||||
maxHeight: '70vh',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
marginBottom: '10px',
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
flexDirection: 'column',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
transition: 'all 0.15s',
|
||||
letterSpacing: '0.06em',
|
||||
'[cmdk-group]': {
|
||||
width: '100%',
|
||||
},
|
||||
'[cmdk-group-heading]': {
|
||||
...displayFlex('start', 'center'),
|
||||
margin: '0 16px',
|
||||
height: '36px',
|
||||
lineHeight: '22px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
},
|
||||
'[cmdk-item]': {
|
||||
margin: '0 4px',
|
||||
},
|
||||
'[aria-selected="true"]': {
|
||||
transition: 'all 0.15s',
|
||||
borderRadius: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
padding: '0 2px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledJumpTo = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('center', 'start'),
|
||||
flexDirection: 'column',
|
||||
padding: '10px 10px 10px 0',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
strong: {
|
||||
fontWeight: '500',
|
||||
marginBottom: '10px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledNotFound = styled('div')(() => {
|
||||
return {
|
||||
width: '612px',
|
||||
...displayFlex('center', 'center'),
|
||||
flexDirection: 'column',
|
||||
padding: '0 16px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '22px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
span: {
|
||||
...displayFlex('flex-start', 'center'),
|
||||
width: '100%',
|
||||
fontWeight: '400',
|
||||
height: '36px',
|
||||
},
|
||||
|
||||
img: {
|
||||
marginTop: '10px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('space-between', 'center'),
|
||||
input: {
|
||||
width: '492px',
|
||||
height: '22px',
|
||||
padding: '0 12px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
...displayFlex('space-between', 'center'),
|
||||
letterSpacing: '0.06em',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
'::placeholder': {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledShortcut = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledLabel = styled('label')(() => {
|
||||
return {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
height: '36px',
|
||||
margin: '12px 16px 0px 16px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
export const StyledModalDivider = styled('div')(() => {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: '0',
|
||||
margin: '6px 16px',
|
||||
borderTop: '0.5px solid var(--affine-border-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalFooter = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'inherit',
|
||||
lineHeight: '22px',
|
||||
marginBottom: '8px',
|
||||
textAlign: 'center',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
...displayFlex('center', 'center'),
|
||||
transition: 'all .15s',
|
||||
'[cmdk-item]': {
|
||||
margin: '0 4px',
|
||||
},
|
||||
'[aria-selected="true"]': {
|
||||
transition: 'all 0.15s',
|
||||
borderRadius: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
'span,svg': {
|
||||
transition: 'all 0.15s',
|
||||
transform: 'scale(1.02)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledModalFooterContent = styled('button')(() => {
|
||||
return {
|
||||
width: '600px',
|
||||
height: '32px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: '22px',
|
||||
textAlign: 'center',
|
||||
...displayFlex('center', 'center'),
|
||||
color: 'inherit',
|
||||
borderRadius: '4px',
|
||||
transition: 'background .15s, color .15s',
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledListItem = styled('button')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
fontSize: 'inherit',
|
||||
color: 'inherit',
|
||||
padding: '0 12px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all .15s',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
span: {
|
||||
...textEllipsis(1),
|
||||
},
|
||||
'> svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
export const CloseIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7.94 7.00014L13.4667 1.47348C13.5759 1.34594 13.633 1.18189 13.6265 1.01411C13.62 0.846324 13.5504 0.687165 13.4317 0.568435C13.313 0.449706 13.1538 0.38015 12.986 0.37367C12.8183 0.367189 12.6542 0.42426 12.5267 0.533477L7 6.06014L1.47334 0.526811C1.3478 0.401275 1.17754 0.33075 1 0.33075C0.822468 0.33075 0.652205 0.401275 0.526669 0.526811C0.401133 0.652346 0.330608 0.82261 0.330608 1.00014C0.330608 1.17768 0.401133 1.34794 0.526669 1.47348L6.06 7.00014L0.526669 12.5268C0.456881 12.5866 0.400201 12.6601 0.360186 12.7428C0.32017 12.8255 0.297683 12.9156 0.294137 13.0074C0.290591 13.0993 0.306061 13.1908 0.339577 13.2764C0.373094 13.3619 0.423932 13.4396 0.488902 13.5046C0.553872 13.5695 0.63157 13.6204 0.71712 13.6539C0.80267 13.6874 0.894225 13.7029 0.986038 13.6993C1.07785 13.6958 1.16794 13.6733 1.25065 13.6333C1.33336 13.5933 1.4069 13.5366 1.46667 13.4668L7 7.94014L12.5267 13.4668C12.6542 13.576 12.8183 13.6331 12.986 13.6266C13.1538 13.6201 13.313 13.5506 13.4317 13.4319C13.5504 13.3131 13.62 13.154 13.6265 12.9862C13.633 12.8184 13.5759 12.6543 13.4667 12.5268L7.94 7.00014Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyboardIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="15"
|
||||
viewBox="0 0 20 15"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17.745 0C18.3417 0 18.914 0.237053 19.336 0.65901C19.7579 1.08097 19.995 1.65326 19.995 2.25V11.755C19.995 12.3517 19.7579 12.924 19.336 13.346C18.914 13.7679 18.3417 14.005 17.745 14.005H2.25C1.95453 14.005 1.66194 13.9468 1.38896 13.8337C1.11598 13.7207 0.867941 13.5549 0.65901 13.346C0.450078 13.1371 0.284344 12.889 0.171271 12.616C0.058198 12.3431 0 12.0505 0 11.755V2.25C0 1.65326 0.237053 1.08097 0.65901 0.65901C1.08097 0.237053 1.65326 0 2.25 0H17.745ZM17.745 1.5H2.25C2.05109 1.5 1.86032 1.57902 1.71967 1.71967C1.57902 1.86032 1.5 2.05109 1.5 2.25V11.755C1.5 12.169 1.836 12.505 2.25 12.505H17.745C17.9439 12.505 18.1347 12.426 18.2753 12.2853C18.416 12.1447 18.495 11.9539 18.495 11.755V2.25C18.495 2.05109 18.416 1.86032 18.2753 1.71967C18.1347 1.57902 17.9439 1.5 17.745 1.5ZM4.75 9.5H15.25C15.44 9.50006 15.6229 9.57224 15.7618 9.70197C15.9006 9.8317 15.9851 10.0093 15.998 10.1989C16.011 10.3885 15.9515 10.5759 15.8316 10.7233C15.7117 10.8707 15.5402 10.9671 15.352 10.993L15.25 11H4.75C4.55998 10.9999 4.37706 10.9278 4.23821 10.798C4.09936 10.6683 4.01493 10.4907 4.00197 10.3011C3.98902 10.1115 4.04852 9.92411 4.16843 9.7767C4.28835 9.62929 4.45975 9.5329 4.648 9.507L4.75 9.5H15.25H4.75ZM14.5 6C14.7652 6 15.0196 6.10536 15.2071 6.29289C15.3946 6.48043 15.5 6.73478 15.5 7C15.5 7.26522 15.3946 7.51957 15.2071 7.70711C15.0196 7.89464 14.7652 8 14.5 8C14.2348 8 13.9804 7.89464 13.7929 7.70711C13.6054 7.51957 13.5 7.26522 13.5 7C13.5 6.73478 13.6054 6.48043 13.7929 6.29289C13.9804 6.10536 14.2348 6 14.5 6ZM8.505 6C8.77022 6 9.02457 6.10536 9.21211 6.29289C9.39964 6.48043 9.505 6.73478 9.505 7C9.505 7.26522 9.39964 7.51957 9.21211 7.70711C9.02457 7.89464 8.77022 8 8.505 8C8.23978 8 7.98543 7.89464 7.79789 7.70711C7.61036 7.51957 7.505 7.26522 7.505 7C7.505 6.73478 7.61036 6.48043 7.79789 6.29289C7.98543 6.10536 8.23978 6 8.505 6ZM5.505 6C5.77022 6 6.02457 6.10536 6.21211 6.29289C6.39964 6.48043 6.505 6.73478 6.505 7C6.505 7.26522 6.39964 7.51957 6.21211 7.70711C6.02457 7.89464 5.77022 8 5.505 8C5.23978 8 4.98543 7.89464 4.79789 7.70711C4.61036 7.51957 4.505 7.26522 4.505 7C4.505 6.73478 4.61036 6.48043 4.79789 6.29289C4.98543 6.10536 5.23978 6 5.505 6ZM11.505 6C11.7702 6 12.0246 6.10536 12.2121 6.29289C12.3996 6.48043 12.505 6.73478 12.505 7C12.505 7.26522 12.3996 7.51957 12.2121 7.70711C12.0246 7.89464 11.7702 8 11.505 8C11.2398 8 10.9854 7.89464 10.7979 7.70711C10.6104 7.51957 10.505 7.26522 10.505 7C10.505 6.73478 10.6104 6.48043 10.7979 6.29289C10.9854 6.10536 11.2398 6 11.505 6ZM4 3C4.26522 3 4.51957 3.10536 4.70711 3.29289C4.89464 3.48043 5 3.73478 5 4C5 4.26522 4.89464 4.51957 4.70711 4.70711C4.51957 4.89464 4.26522 5 4 5C3.73478 5 3.48043 4.89464 3.29289 4.70711C3.10536 4.51957 3 4.26522 3 4C3 3.73478 3.10536 3.48043 3.29289 3.29289C3.48043 3.10536 3.73478 3 4 3ZM6.995 3C7.26022 3 7.51457 3.10536 7.70211 3.29289C7.88964 3.48043 7.995 3.73478 7.995 4C7.995 4.26522 7.88964 4.51957 7.70211 4.70711C7.51457 4.89464 7.26022 5 6.995 5C6.72978 5 6.47543 4.89464 6.28789 4.70711C6.10036 4.51957 5.995 4.26522 5.995 4C5.995 3.73478 6.10036 3.48043 6.28789 3.29289C6.47543 3.10536 6.72978 3 6.995 3ZM9.995 3C10.2602 3 10.5146 3.10536 10.7021 3.29289C10.8896 3.48043 10.995 3.73478 10.995 4C10.995 4.26522 10.8896 4.51957 10.7021 4.70711C10.5146 4.89464 10.2602 5 9.995 5C9.72978 5 9.47543 4.89464 9.28789 4.70711C9.10036 4.51957 8.995 4.26522 8.995 4C8.995 3.73478 9.10036 3.48043 9.28789 3.29289C9.47543 3.10536 9.72978 3 9.995 3ZM12.995 3C13.2602 3 13.5146 3.10536 13.7021 3.29289C13.8896 3.48043 13.995 3.73478 13.995 4C13.995 4.26522 13.8896 4.51957 13.7021 4.70711C13.5146 4.89464 13.2602 5 12.995 5C12.7298 5 12.4754 4.89464 12.2879 4.70711C12.1004 4.51957 11.995 4.26522 11.995 4C11.995 3.73478 12.1004 3.48043 12.2879 3.29289C12.4754 3.10536 12.7298 3 12.995 3ZM15.995 3C16.2602 3 16.5146 3.10536 16.7021 3.29289C16.8896 3.48043 16.995 3.73478 16.995 4C16.995 4.26522 16.8896 4.51957 16.7021 4.70711C16.5146 4.89464 16.2602 5 15.995 5C15.7298 5 15.4754 4.89464 15.2879 4.70711C15.1004 4.51957 14.995 4.26522 14.995 4C14.995 3.73478 15.1004 3.48043 15.2879 3.29289C15.4754 3.10536 15.7298 3 15.995 3Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
ModalCloseButton,
|
||||
MuiClickAwayListener,
|
||||
MuiSlide,
|
||||
} from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import {
|
||||
useEdgelessShortcuts,
|
||||
useGeneralShortcuts,
|
||||
useMarkdownShortcuts,
|
||||
usePageShortcuts,
|
||||
} from '../../../hooks/affine/use-shortcuts';
|
||||
import { KeyboardIcon } from './icons';
|
||||
import {
|
||||
StyledListItem,
|
||||
StyledModalHeader,
|
||||
StyledShortcutsModal,
|
||||
StyledSubTitle,
|
||||
StyledTitle,
|
||||
} from './style';
|
||||
type ModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const markdownShortcuts = useMarkdownShortcuts();
|
||||
const pageShortcuts = usePageShortcuts();
|
||||
const edgelessShortcuts = useEdgelessShortcuts();
|
||||
const generalShortcuts = useGeneralShortcuts();
|
||||
|
||||
return (
|
||||
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
|
||||
<StyledShortcutsModal data-testid="shortcuts-modal">
|
||||
<MuiClickAwayListener
|
||||
onClickAway={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<StyledModalHeader>
|
||||
<StyledTitle>
|
||||
<KeyboardIcon />
|
||||
{t['Shortcuts']()}
|
||||
</StyledTitle>
|
||||
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
size={[24, 24]}
|
||||
iconSize={[15, 15]}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</StyledModalHeader>
|
||||
<StyledSubTitle style={{ marginTop: 0 }}>
|
||||
{t['General']()}
|
||||
</StyledSubTitle>
|
||||
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
<span>{shortcuts}</span>
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
<StyledSubTitle>{t['Page']()}</StyledSubTitle>
|
||||
{Object.entries(pageShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
<span>{shortcuts}</span>
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
<StyledSubTitle>{t['Edgeless']()}</StyledSubTitle>
|
||||
{Object.entries(edgelessShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
<span>{shortcuts}</span>
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
<StyledSubTitle>{t['Markdown Syntax']()}</StyledSubTitle>
|
||||
{Object.entries(markdownShortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<StyledListItem key={title}>
|
||||
<span>{title}</span>
|
||||
<span>{shortcuts}</span>
|
||||
</StyledListItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
</StyledShortcutsModal>
|
||||
</MuiSlide>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
|
||||
export const StyledShortcutsModal = styled('div')(() => ({
|
||||
width: '288px',
|
||||
height: '74vh',
|
||||
paddingBottom: '28px',
|
||||
backgroundColor: 'var(--affine-white)',
|
||||
boxShadow: 'var(--affine-popover-shadow)',
|
||||
borderRadius: `var(--affine-popover-radius)`,
|
||||
overflow: 'auto',
|
||||
boxRadius: '10px',
|
||||
position: 'fixed',
|
||||
right: '12px',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
zIndex: 'var(--affine-z-index-modal)',
|
||||
}));
|
||||
export const StyledTitle = styled('div')(() => ({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '500',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
height: '44px',
|
||||
...displayFlex('center', 'center'),
|
||||
svg: {
|
||||
width: '20px',
|
||||
marginRight: '14px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
}));
|
||||
export const StyledSubTitle = styled('div')(() => ({
|
||||
fontWeight: '500',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
height: '34px',
|
||||
lineHeight: '36px',
|
||||
marginTop: '28px',
|
||||
padding: '0 16px',
|
||||
}));
|
||||
export const StyledModalHeader = styled('div')(() => ({
|
||||
...displayFlex('space-between', 'center'),
|
||||
paddingTop: '8px 4px 0 4px',
|
||||
width: '100%',
|
||||
padding: '8px 16px 0 16px',
|
||||
position: 'sticky',
|
||||
left: '0',
|
||||
top: '0',
|
||||
background: 'var(--affine-white)',
|
||||
transition: 'background-color 0.5s',
|
||||
}));
|
||||
|
||||
export const StyledListItem = styled('div')(() => ({
|
||||
height: '34px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
padding: '0 16px',
|
||||
}));
|
||||
@@ -0,0 +1,235 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalWrapper,
|
||||
Tooltip,
|
||||
} from '@affine/component';
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { Footer } from '../footer';
|
||||
import {
|
||||
StyledCreateWorkspaceCard,
|
||||
StyledCreateWorkspaceCardPill,
|
||||
StyledCreateWorkspaceCardPillContainer,
|
||||
StyledCreateWorkspaceCardPillContent,
|
||||
StyledCreateWorkspaceCardPillIcon,
|
||||
StyledCreateWorkspaceCardPillTextSecondary,
|
||||
StyledHelperContainer,
|
||||
StyledModalContent,
|
||||
StyledModalHeader,
|
||||
StyledModalHeaderLeft,
|
||||
StyledModalTitle,
|
||||
StyledOperationWrapper,
|
||||
StyleWorkspaceAdd,
|
||||
StyleWorkspaceInfo,
|
||||
StyleWorkspaceTitle,
|
||||
} from './styles';
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
workspaces: RootWorkspaceMetadata[];
|
||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
onMoveWorkspace: (activeId: string, overId: string) => void;
|
||||
}
|
||||
|
||||
const CreateWorkspaceCard = ({
|
||||
onNewWorkspace,
|
||||
onAddWorkspace,
|
||||
}: {
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const anchorEL = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (runtimeConfig.enableSQLiteProvider && environment.isDesktop) {
|
||||
return (
|
||||
<Menu
|
||||
placement="auto"
|
||||
trigger={['click']}
|
||||
zIndex={1000}
|
||||
content={
|
||||
<StyledCreateWorkspaceCardPillContainer>
|
||||
<StyledCreateWorkspaceCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
onClick={onNewWorkspace}
|
||||
data-testid="new-workspace"
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<div>
|
||||
<p>{t['New Workspace']()}</p>
|
||||
<StyledCreateWorkspaceCardPillTextSecondary>
|
||||
<p>{t['Create your own workspace']()}</p>
|
||||
</StyledCreateWorkspaceCardPillTextSecondary>
|
||||
</div>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<PlusIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledCreateWorkspaceCardPill>
|
||||
<StyledCreateWorkspaceCardPill>
|
||||
<MenuItem
|
||||
onClick={onAddWorkspace}
|
||||
data-testid="add-workspace"
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<div>
|
||||
<p>{t['Add Workspace']()}</p>
|
||||
<StyledCreateWorkspaceCardPillTextSecondary>
|
||||
<p>{t['Add Workspace Hint']()}</p>
|
||||
</StyledCreateWorkspaceCardPillTextSecondary>
|
||||
</div>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<ImportIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledCreateWorkspaceCardPill>
|
||||
</StyledCreateWorkspaceCardPillContainer>
|
||||
}
|
||||
>
|
||||
<StyledCreateWorkspaceCard
|
||||
ref={anchorEL}
|
||||
data-testid="add-or-new-workspace"
|
||||
>
|
||||
<StyleWorkspaceAdd className="add-icon">
|
||||
<PlusIcon />
|
||||
</StyleWorkspaceAdd>
|
||||
|
||||
<StyleWorkspaceInfo>
|
||||
<StyleWorkspaceTitle>{t['New Workspace']()}</StyleWorkspaceTitle>
|
||||
<p>{t['Create Or Import']()}</p>
|
||||
</StyleWorkspaceInfo>
|
||||
</StyledCreateWorkspaceCard>
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<StyledCreateWorkspaceCard
|
||||
onClick={onNewWorkspace}
|
||||
data-testid="new-workspace"
|
||||
>
|
||||
<StyleWorkspaceAdd className="add-icon">
|
||||
<PlusIcon />
|
||||
</StyleWorkspaceAdd>
|
||||
|
||||
<StyleWorkspaceInfo>
|
||||
<StyleWorkspaceTitle>{t['New Workspace']()}</StyleWorkspaceTitle>
|
||||
<p>{t['Create Or Import']()}</p>
|
||||
</StyleWorkspaceInfo>
|
||||
</StyledCreateWorkspaceCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const WorkspaceListModal = ({
|
||||
disabled,
|
||||
open,
|
||||
onClose,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
onNewWorkspace,
|
||||
onAddWorkspace,
|
||||
currentWorkspaceId,
|
||||
onMoveWorkspace,
|
||||
}: WorkspaceModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper
|
||||
width={720}
|
||||
height={690}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<StyledModalHeaderLeft>
|
||||
<StyledModalTitle>{t['My Workspaces']()}</StyledModalTitle>
|
||||
<Tooltip
|
||||
content={t['Workspace description']()}
|
||||
placement="top-start"
|
||||
disablePortal={true}
|
||||
>
|
||||
<StyledHelperContainer>
|
||||
<HelpIcon />
|
||||
</StyledHelperContainer>
|
||||
</Tooltip>
|
||||
</StyledModalHeaderLeft>
|
||||
|
||||
<StyledOperationWrapper>
|
||||
<ModalCloseButton
|
||||
data-testid="close-workspace-modal"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
absolute={false}
|
||||
/>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
<ScrollableContainer>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onDragEnd={useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over?.id) {
|
||||
onMoveWorkspace(active.id as string, over?.id as string);
|
||||
}
|
||||
},
|
||||
[onMoveWorkspace]
|
||||
)}
|
||||
/>
|
||||
<CreateWorkspaceCard
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
/>
|
||||
</StyledModalContent>
|
||||
</ScrollableContainer>
|
||||
<Footer />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
width: '202px',
|
||||
p: {
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
},
|
||||
svg: {
|
||||
marginRight: '10px',
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
span: {
|
||||
flexGrow: 1,
|
||||
...textEllipsis(1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
marginBottom: '10px',
|
||||
maxWidth: '200px',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCard = styled('div')(() => {
|
||||
return {
|
||||
width: '310px',
|
||||
height: '124px',
|
||||
marginBottom: '24px',
|
||||
cursor: 'pointer',
|
||||
padding: '16px',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
borderRadius: '12px',
|
||||
transition: 'all .1s',
|
||||
background: 'var(--affine-white-80)',
|
||||
...displayFlex('flex-start', 'flex-start'),
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
'.add-icon': {
|
||||
borderColor: 'var(--affine-white)',
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
'@media (max-width: 720px)': {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||
return {
|
||||
padding: '12px',
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
margin: '-8px -4px',
|
||||
flexFlow: 'column',
|
||||
gap: '12px',
|
||||
background: 'var(--affine-background-overlay-panel-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPill = styled('div')(() => {
|
||||
return {
|
||||
borderRadius: '5px',
|
||||
display: 'flex',
|
||||
boxShadow: '0px 0px 6px 0px rgba(0, 0, 0, 0.1)',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPillContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPillIcon = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '20px',
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCreateWorkspaceCardPillTextSecondary = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '12px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeaderLeft = styled('div')(() => {
|
||||
return { ...displayFlex('flex-start', 'center') };
|
||||
});
|
||||
export const StyledModalTitle = styled('div')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledHelperContainer = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginLeft: '15px',
|
||||
fontWeight: 400,
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalContent = styled('div')({
|
||||
height: '540px',
|
||||
padding: '8px 40px',
|
||||
...displayFlex('space-between', 'flex-start', 'flex-start'),
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
export const StyledOperationWrapper = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('flex-end', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceAdd = styled('div')(() => {
|
||||
return {
|
||||
width: '58px',
|
||||
height: '58px',
|
||||
borderRadius: '100%',
|
||||
background: 'var(--affine-white-80)',
|
||||
border: '1.5px dashed #f4f5fa',
|
||||
transition: 'background .2s',
|
||||
fontSize: '24px',
|
||||
...displayFlex('center', 'center'),
|
||||
borderColor: 'var(--affine-white)',
|
||||
color: 'var(--affine-primary-color)',
|
||||
};
|
||||
});
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
marginTop: '10px',
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '10px 40px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const workspaceAvatarStyle = style({
|
||||
flexShrink: 0,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './workspace-selector';
|
||||
@@ -0,0 +1,48 @@
|
||||
import { displayFlex, textEllipsis } from '@affine/component';
|
||||
import { styled } from '@affine/component';
|
||||
export const StyledSelectorContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '58px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 6px',
|
||||
margin: '0 -6px',
|
||||
borderRadius: '8px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSelectorWrapper = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '8px',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
});
|
||||
export const StyledWorkspaceName = styled('div')(() => {
|
||||
return {
|
||||
lineHeight: '24px',
|
||||
fontWeight: 600,
|
||||
userSelect: 'none',
|
||||
...textEllipsis(1),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceStatus = styled('div')(() => {
|
||||
return {
|
||||
height: '22px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
userSelect: 'none',
|
||||
svg: {
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
marginRight: '4px',
|
||||
},
|
||||
};
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import type { AllWorkspace } from '../../../../shared';
|
||||
import { workspaceAvatarStyle } from './index.css';
|
||||
import {
|
||||
StyledSelectorContainer,
|
||||
StyledSelectorWrapper,
|
||||
StyledWorkspaceName,
|
||||
StyledWorkspaceStatus,
|
||||
} from './styles';
|
||||
|
||||
export type WorkspaceSelectorProps = {
|
||||
currentWorkspace: AllWorkspace;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo-Doma Co-locate WorkspaceListModal with {@link WorkspaceSelector},
|
||||
* because it's never used elsewhere.
|
||||
*/
|
||||
export const WorkspaceSelector: React.FC<WorkspaceSelectorProps> = ({
|
||||
currentWorkspace,
|
||||
onClick,
|
||||
}) => {
|
||||
const [name] = useBlockSuiteWorkspaceName(
|
||||
currentWorkspace?.blockSuiteWorkspace
|
||||
);
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
|
||||
// Open dialog when `Enter` or `Space` pressed
|
||||
// TODO-Doma Refactor with `@radix-ui/react-dialog` or other libraries that handle these out of the box and be accessible by default
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
// TODO-Doma Rename this callback to `onOpenDialog` or something to reduce ambiguity.
|
||||
onClick();
|
||||
}
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSelectorContainer
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-testid="current-workspace"
|
||||
>
|
||||
<WorkspaceAvatar
|
||||
data-testid="workspace-avatar"
|
||||
className={workspaceAvatarStyle}
|
||||
size={40}
|
||||
workspace={currentWorkspace?.blockSuiteWorkspace ?? null}
|
||||
/>
|
||||
<StyledSelectorWrapper>
|
||||
<StyledWorkspaceName data-testid="workspace-name">
|
||||
{name}
|
||||
</StyledWorkspaceName>
|
||||
{workspace && (
|
||||
<StyledWorkspaceStatus>
|
||||
{workspace.flavour === 'local' ? (
|
||||
<LocalWorkspaceIcon />
|
||||
) : (
|
||||
<CloudWorkspaceIcon />
|
||||
)}
|
||||
{workspace.flavour === 'local' ? 'Local' : 'AFFiNE Cloud'}
|
||||
</StyledWorkspaceStatus>
|
||||
)}
|
||||
</StyledSelectorWrapper>
|
||||
</StyledSelectorContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,271 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { MenuItem } from '@affine/component/app-sidebar';
|
||||
import {
|
||||
EditCollectionModel,
|
||||
useCollectionManager,
|
||||
useSavedCollections,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteIcon,
|
||||
FilterIcon,
|
||||
MoreHorizontalIcon,
|
||||
UnpinIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
|
||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||
import { filterPage } from '../../../../utils/filter';
|
||||
import type { CollectionsListProps } from '../index';
|
||||
import { Page } from './page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const Collections_DROP_AREA_PREFIX = 'collections-';
|
||||
const isCollectionsDropArea = (id?: string | number) => {
|
||||
return typeof id === 'string' && id.startsWith(Collections_DROP_AREA_PREFIX);
|
||||
};
|
||||
export const processCollectionsDrag = (e: DragEndEvent) => {
|
||||
if (
|
||||
isCollectionsDropArea(e.over?.id) &&
|
||||
String(e.active.id).startsWith('page-list-item-')
|
||||
) {
|
||||
e.over?.data.current?.addToCollection?.(e.active.data.current?.pageId);
|
||||
}
|
||||
};
|
||||
const CollectionOperations = ({
|
||||
view,
|
||||
showUpdateCollection,
|
||||
setting,
|
||||
}: {
|
||||
view: Collection;
|
||||
showUpdateCollection: () => void;
|
||||
setting: ReturnType<typeof useCollectionManager>;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const actions = useMemo<
|
||||
Array<
|
||||
| {
|
||||
icon: ReactElement;
|
||||
name: string;
|
||||
click: () => void;
|
||||
className?: string;
|
||||
element?: undefined;
|
||||
}
|
||||
| {
|
||||
element: ReactElement;
|
||||
}
|
||||
>
|
||||
>(
|
||||
() => [
|
||||
{
|
||||
icon: <FilterIcon />,
|
||||
name: t['Edit Filter'](),
|
||||
click: showUpdateCollection,
|
||||
},
|
||||
{
|
||||
icon: <UnpinIcon />,
|
||||
name: t['Unpin'](),
|
||||
click: () => {
|
||||
return setting.updateCollection({
|
||||
...view,
|
||||
pinned: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
element: <div key="divider" className={styles.menuDividerStyle}></div>,
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon />,
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
return setting.deleteCollection(view.id);
|
||||
},
|
||||
className: styles.deleteFolder,
|
||||
},
|
||||
],
|
||||
[setting, showUpdateCollection, t, view]
|
||||
);
|
||||
return (
|
||||
<div style={{ minWidth: 150 }}>
|
||||
{actions.map(action => {
|
||||
if (action.element) {
|
||||
return action.element;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="collection-option"
|
||||
key={action.name}
|
||||
className={action.className}
|
||||
icon={action.icon}
|
||||
onClick={action.click}
|
||||
>
|
||||
{action.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CollectionRenderer = ({
|
||||
collection,
|
||||
pages,
|
||||
workspace,
|
||||
getPageInfo,
|
||||
}: {
|
||||
collection: Collection;
|
||||
pages: PageMeta[];
|
||||
workspace: Workspace;
|
||||
getPageInfo: GetPageInfoById;
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = React.useState(true);
|
||||
const setting = useCollectionManager(workspace.id);
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
const clickCollection = useCallback(() => {
|
||||
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||
setting.selectCollection(collection.id);
|
||||
}, [jumpToSubPath, workspace.id, setting, collection.id]);
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: `${Collections_DROP_AREA_PREFIX}${collection.id}`,
|
||||
data: {
|
||||
addToCollection: (id: string) => {
|
||||
setting.addPage(collection.id, id).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const allPagesMeta = useMemo(
|
||||
() => Object.fromEntries(pages.map(v => [v.id, v])),
|
||||
[pages]
|
||||
);
|
||||
const [show, showUpdateCollection] = useState(false);
|
||||
const allowList = useMemo(
|
||||
() => new Set(collection.allowList),
|
||||
[collection.allowList]
|
||||
);
|
||||
const excludeList = useMemo(
|
||||
() => new Set(collection.excludeList),
|
||||
[collection.excludeList]
|
||||
);
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
return setting.updateCollection({
|
||||
...collection,
|
||||
allowList: collection.allowList?.filter(v => v != id),
|
||||
});
|
||||
},
|
||||
[collection, setting]
|
||||
);
|
||||
const addToExcludeList = useCallback(
|
||||
(id: string) => {
|
||||
return setting.updateCollection({
|
||||
...collection,
|
||||
excludeList: [id, ...(collection.excludeList ?? [])],
|
||||
});
|
||||
},
|
||||
[collection, setting]
|
||||
);
|
||||
const pagesToRender = pages.filter(
|
||||
page => filterPage(collection, page) && !page.trash
|
||||
);
|
||||
return (
|
||||
<Collapsible.Root open={!collapsed}>
|
||||
<EditCollectionModel
|
||||
propertiesMeta={workspace.meta.properties}
|
||||
getPageInfo={getPageInfo}
|
||||
init={collection}
|
||||
onConfirm={setting.saveCollection}
|
||||
open={show}
|
||||
onClose={() => showUpdateCollection(false)}
|
||||
/>
|
||||
<MenuItem
|
||||
data-testid="collection-item"
|
||||
ref={setNodeRef}
|
||||
onCollapsedChange={setCollapsed}
|
||||
active={isOver}
|
||||
icon={<ViewLayersIcon />}
|
||||
postfix={
|
||||
<Menu
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<CollectionOperations
|
||||
view={collection}
|
||||
showUpdateCollection={() => showUpdateCollection(true)}
|
||||
setting={setting}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div data-testid="collection-options" className={styles.more}>
|
||||
<MoreHorizontalIcon></MoreHorizontalIcon>
|
||||
</div>
|
||||
</Menu>
|
||||
}
|
||||
collapsed={pagesToRender.length > 0 ? collapsed : undefined}
|
||||
onClick={clickCollection}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div>{collection.name}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<Collapsible.Content>
|
||||
<div style={{ marginLeft: 8 }}>
|
||||
{pagesToRender.map(page => {
|
||||
return (
|
||||
<Page
|
||||
inAllowList={allowList.has(page.id)}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
inExcludeList={excludeList.has(page.id)}
|
||||
addToExcludeList={addToExcludeList}
|
||||
allPageMeta={allPagesMeta}
|
||||
page={page}
|
||||
key={page.id}
|
||||
workspace={workspace}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
export const CollectionsList = ({ workspace }: CollectionsListProps) => {
|
||||
const metas = useBlockSuitePageMeta(workspace);
|
||||
const { savedCollections } = useSavedCollections(workspace.id);
|
||||
const getPageInfo = useGetPageInfoById(workspace);
|
||||
return (
|
||||
<div data-testid="collections" className={styles.wrapper}>
|
||||
{savedCollections
|
||||
.filter(v => v.pinned)
|
||||
.map(view => {
|
||||
return (
|
||||
<CollectionRenderer
|
||||
getPageInfo={getPageInfo}
|
||||
key={view.id}
|
||||
collection={view}
|
||||
pages={metas}
|
||||
workspace={workspace}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './collections-list';
|
||||
export { Page } from './page';
|
||||
export { PageOperations } from './page';
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Menu } from '@affine/component';
|
||||
import { MenuItem } from '@affine/component/app-sidebar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteIcon,
|
||||
EdgelessIcon,
|
||||
FilterMinusIcon,
|
||||
FilterUndoIcon,
|
||||
MoreHorizontalIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { pageSettingFamily } from '../../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||
import { ReferencePage } from '../components/reference-page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const PageOperations = ({
|
||||
page,
|
||||
inAllowList,
|
||||
addToExcludeList,
|
||||
removeFromAllowList,
|
||||
inExcludeList,
|
||||
workspace,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
page: PageMeta;
|
||||
inAllowList: boolean;
|
||||
removeFromAllowList: (id: string) => void;
|
||||
inExcludeList: boolean;
|
||||
addToExcludeList: (id: string) => void;
|
||||
}) => {
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(workspace);
|
||||
const t = useAFFiNEI18N();
|
||||
const actions = useMemo<
|
||||
Array<
|
||||
| {
|
||||
icon: ReactElement;
|
||||
name: string;
|
||||
click: () => void;
|
||||
className?: string;
|
||||
element?: undefined;
|
||||
}
|
||||
| {
|
||||
element: ReactElement;
|
||||
}
|
||||
>
|
||||
>(
|
||||
() => [
|
||||
...(inAllowList
|
||||
? [
|
||||
{
|
||||
icon: <FilterMinusIcon />,
|
||||
name: t['Remove special filter'](),
|
||||
click: () => removeFromAllowList(page.id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!inExcludeList
|
||||
? [
|
||||
{
|
||||
icon: <FilterUndoIcon />,
|
||||
name: t['Exclude from filter'](),
|
||||
click: () => addToExcludeList(page.id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
element: <div key="divider" className={styles.menuDividerStyle}></div>,
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon />,
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
removeToTrash(page.id);
|
||||
},
|
||||
className: styles.deleteFolder,
|
||||
},
|
||||
],
|
||||
[
|
||||
inAllowList,
|
||||
t,
|
||||
inExcludeList,
|
||||
removeFromAllowList,
|
||||
page.id,
|
||||
addToExcludeList,
|
||||
removeToTrash,
|
||||
]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{actions.map(action => {
|
||||
if (action.element) {
|
||||
return action.element;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="collection-page-option"
|
||||
key={action.name}
|
||||
className={action.className}
|
||||
icon={action.icon}
|
||||
onClick={action.click}
|
||||
>
|
||||
{action.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const Page = ({
|
||||
page,
|
||||
workspace,
|
||||
allPageMeta,
|
||||
inAllowList,
|
||||
inExcludeList,
|
||||
removeFromAllowList,
|
||||
addToExcludeList,
|
||||
}: {
|
||||
page: PageMeta;
|
||||
inAllowList: boolean;
|
||||
removeFromAllowList: (id: string) => void;
|
||||
inExcludeList: boolean;
|
||||
addToExcludeList: (id: string) => void;
|
||||
workspace: Workspace;
|
||||
allPageMeta: Record<string, PageMeta>;
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = React.useState(true);
|
||||
const params = useParams();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
const pageId = page.id;
|
||||
const active = params.pageId === pageId;
|
||||
const setting = useAtomValue(pageSettingFamily(pageId));
|
||||
const icon = setting?.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
const references = useBlockSuitePageReferences(workspace, pageId);
|
||||
const clickPage = useCallback(() => {
|
||||
jumpToPage(workspace.id, page.id);
|
||||
}, [jumpToPage, page.id, workspace.id]);
|
||||
const referencesToRender = references.filter(id => !allPageMeta[id]?.trash);
|
||||
return (
|
||||
<Collapsible.Root open={!collapsed}>
|
||||
<MenuItem
|
||||
data-testid="collection-page"
|
||||
icon={icon}
|
||||
onClick={clickPage}
|
||||
className={styles.title}
|
||||
active={active}
|
||||
collapsed={referencesToRender.length > 0 ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
postfix={
|
||||
<Menu
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<div style={{ width: 220 }}>
|
||||
<PageOperations
|
||||
inAllowList={inAllowList}
|
||||
removeFromAllowList={removeFromAllowList}
|
||||
inExcludeList={inExcludeList}
|
||||
addToExcludeList={addToExcludeList}
|
||||
page={page}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div data-testid="collection-page-options" className={styles.more}>
|
||||
<MoreHorizontalIcon></MoreHorizontalIcon>
|
||||
</div>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
{page.title || t['Untitled']()}
|
||||
</MenuItem>
|
||||
<Collapsible.Content>
|
||||
<div style={{ marginLeft: 8 }}>
|
||||
{referencesToRender.map(id => {
|
||||
return (
|
||||
<ReferencePage
|
||||
key={id}
|
||||
workspace={workspace}
|
||||
pageId={id}
|
||||
metaMapping={allPageMeta}
|
||||
parentIds={new Set([pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const wrapper = style({
|
||||
userSelect: 'none',
|
||||
// marginLeft:8,
|
||||
});
|
||||
export const collapsedIcon = style({
|
||||
transition: 'transform 0.2s ease-in-out',
|
||||
selectors: {
|
||||
'&[data-collapsed="true"]': {
|
||||
transform: 'rotate(-90deg)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const view = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const viewTitle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const title = style({
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
});
|
||||
export const more = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
padding: 4,
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
export const deleteFolder = style({
|
||||
':hover': {
|
||||
color: 'var(--affine-error-color)',
|
||||
backgroundColor: 'var(--affine-background-error-color)',
|
||||
},
|
||||
});
|
||||
globalStyle(`${deleteFolder}:hover svg`, {
|
||||
color: 'var(--affine-error-color)',
|
||||
});
|
||||
export const menuDividerStyle = style({
|
||||
marginTop: '2px',
|
||||
marginBottom: '2px',
|
||||
marginLeft: '12px',
|
||||
marginRight: '8px',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { pageSettingFamily } from '../../../../atoms';
|
||||
import * as styles from '../favorite/styles.css';
|
||||
interface ReferencePageProps {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
metaMapping: Record<string, PageMeta>;
|
||||
parentIds: Set<string>;
|
||||
}
|
||||
|
||||
export const ReferencePage = ({
|
||||
workspace,
|
||||
pageId,
|
||||
metaMapping,
|
||||
parentIds,
|
||||
}: ReferencePageProps) => {
|
||||
const params = useParams();
|
||||
const setting = useAtomValue(pageSettingFamily(pageId));
|
||||
const active = params.pageId === pageId;
|
||||
const icon = setting?.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />;
|
||||
const references = useBlockSuitePageReferences(workspace, pageId);
|
||||
const referencesToShow = useMemo(() => {
|
||||
return [
|
||||
...new Set(
|
||||
references.filter(
|
||||
ref => !parentIds.has(ref) && !metaMapping[ref]?.trash
|
||||
)
|
||||
),
|
||||
];
|
||||
}, [references, parentIds, metaMapping]);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0;
|
||||
const nestedItem = parentIds.size > 0;
|
||||
const untitled = !metaMapping[pageId]?.title;
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Collapsible.Root
|
||||
className={styles.favItemWrapper}
|
||||
data-nested={nestedItem}
|
||||
open={!collapsed}
|
||||
>
|
||||
<MenuLinkItem
|
||||
data-type="favorite-list-item"
|
||||
data-testid={`favorite-list-item-${pageId}`}
|
||||
active={active}
|
||||
href={`/workspace/${workspace.id}/${pageId}`}
|
||||
icon={icon}
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
>
|
||||
<span className={styles.label} data-untitled={untitled}>
|
||||
{metaMapping[pageId]?.title || t['Untitled']()}
|
||||
</span>
|
||||
</MenuLinkItem>
|
||||
{collapsible && (
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
<div className={styles.collapsibleContentInner}>
|
||||
{referencesToShow.map(ref => {
|
||||
return (
|
||||
<ReferencePage
|
||||
key={ref}
|
||||
workspace={workspace}
|
||||
pageId={ref}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([...parentIds, pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
)}
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MenuItem } from '@affine/component/app-sidebar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
export const EmptyItem = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<MenuItem disabled={true}>{t['Favorite pages for easy access']()}</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyItem;
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { ReferencePage } from '../components/reference-page';
|
||||
import type { FavoriteListProps } from '../index';
|
||||
import EmptyItem from './empty-item';
|
||||
|
||||
export const FavoriteList = ({ workspace }: FavoriteListProps) => {
|
||||
const metas = useBlockSuitePageMeta(workspace);
|
||||
|
||||
const favoriteList = useMemo(
|
||||
() => metas.filter(p => p.favorite && !p.trash),
|
||||
[metas]
|
||||
);
|
||||
|
||||
const metaMapping = useMemo(
|
||||
() =>
|
||||
metas.reduce(
|
||||
(acc, meta) => {
|
||||
acc[meta.id] = meta;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, PageMeta>
|
||||
),
|
||||
[metas]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{favoriteList.map((pageMeta, index) => {
|
||||
return (
|
||||
<ReferencePage
|
||||
key={`${pageMeta}-${index}`}
|
||||
metaMapping={metaMapping}
|
||||
pageId={pageMeta.id}
|
||||
// memo?
|
||||
parentIds={new Set()}
|
||||
workspace={workspace}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{favoriteList.length === 0 && <EmptyItem />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoriteList;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './favorite-list';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
export const label = style({
|
||||
selectors: {
|
||||
'&[data-untitled="true"]': {
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const favItemWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
selectors: {
|
||||
'&[data-nested="true"]': {
|
||||
marginLeft: '12px',
|
||||
width: 'calc(100% - 12px)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const slideDown = keyframes({
|
||||
'0%': {
|
||||
height: '0px',
|
||||
},
|
||||
'100%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
});
|
||||
|
||||
const slideUp = keyframes({
|
||||
'0%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
'100%': {
|
||||
height: '0px',
|
||||
},
|
||||
});
|
||||
|
||||
export const collapsibleContent = style({
|
||||
overflow: 'hidden',
|
||||
selectors: {
|
||||
'&[data-state="open"]': {
|
||||
animation: `${slideDown} 0.2s ease-out`,
|
||||
},
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${slideUp} 0.2s ease-out`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const collapsibleContentInner = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
|
||||
export type FavoriteListProps = {
|
||||
workspace: Workspace;
|
||||
};
|
||||
|
||||
export type CollectionsListProps = {
|
||||
workspace: Workspace;
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import { alpha, displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledListItem = styled('div')<{
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(({ active, disabled }) => {
|
||||
return {
|
||||
height: '32px',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
paddingLeft: '2px',
|
||||
paddingRight: '2px',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '4px',
|
||||
position: 'relative',
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
...displayFlex('flex-start', 'stretch'),
|
||||
...(disabled
|
||||
? {
|
||||
cursor: 'not-allowed',
|
||||
color: 'var(--affine-border-color)',
|
||||
}
|
||||
: {}),
|
||||
|
||||
'a > svg, div > svg': {
|
||||
fontSize: '20px',
|
||||
marginLeft: '14px',
|
||||
marginRight: '12px',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
},
|
||||
':hover:not([disabled])': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCollapseButton = styled('button')<{
|
||||
collapse: boolean;
|
||||
show?: boolean;
|
||||
}>(({ collapse, show = true }) => {
|
||||
return {
|
||||
width: '16px',
|
||||
height: '100%',
|
||||
...displayFlex('center', 'center'),
|
||||
fontSize: '16px',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
color: 'var(--affine-icon-color)',
|
||||
opacity: '.6',
|
||||
transition: 'opacity .15s ease-in-out',
|
||||
display: show ? 'flex' : 'none',
|
||||
svg: {
|
||||
transform: `rotate(${collapse ? '0' : '-90'}deg)`,
|
||||
},
|
||||
':hover': {
|
||||
opacity: '1',
|
||||
},
|
||||
':focus-visible': {
|
||||
outline: '-webkit-focus-ring-color auto 1px',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCollapseItem = styled('div')<{
|
||||
disable?: boolean;
|
||||
active?: boolean;
|
||||
isOver?: boolean;
|
||||
textWrap?: boolean;
|
||||
}>(({ disable = false, active = false, isOver, textWrap = false }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
lineHeight: '1.5',
|
||||
minHeight: '32px',
|
||||
borderRadius: '8px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
paddingRight: '2px',
|
||||
position: 'relative',
|
||||
color: disable
|
||||
? 'var(--affine-text-disable-color)'
|
||||
: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
cursor: disable ? 'not-allowed' : 'pointer',
|
||||
background: isOver ? alpha('var(--affine-primary-color)', 0.06) : '',
|
||||
userSelect: 'none',
|
||||
...(textWrap
|
||||
? {
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}
|
||||
: {}),
|
||||
span: {
|
||||
flexGrow: '1',
|
||||
textAlign: 'left',
|
||||
...textEllipsis(1),
|
||||
},
|
||||
'> svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '8px',
|
||||
flexShrink: '0',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
},
|
||||
|
||||
':hover': disable
|
||||
? {}
|
||||
: {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
'.operation-button': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledRouteNavigationWrapper = styled('div')({
|
||||
height: '32px',
|
||||
width: '80px',
|
||||
marginRight: '16px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const StyledSliderBarInnerWrapper = styled('div')(() => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
margin: '0 2px',
|
||||
position: 'relative',
|
||||
height: 'calc(100% - 52px * 2)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledLink = styled(Link)(() => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
textAlign: 'left',
|
||||
color: 'inherit',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
':visited': {
|
||||
color: 'inherit',
|
||||
},
|
||||
overflow: 'hidden',
|
||||
div: {
|
||||
wordBreak: 'break-all',
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'nowrap',
|
||||
...textEllipsis(1),
|
||||
},
|
||||
userDrag: 'none',
|
||||
userSelect: 'none',
|
||||
appRegion: 'no-drag',
|
||||
WebkitUserSelect: 'none',
|
||||
WebkitUserDrag: 'none',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
};
|
||||
});
|
||||
export const StyledNewPageButton = styled('button')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '52px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
padding: '0 16px',
|
||||
svg: {
|
||||
fontSize: '20px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':hover': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
svg: {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledSliderModalBackground = styled('div')<{ active: boolean }>(({
|
||||
active,
|
||||
}) => {
|
||||
return {
|
||||
transition: 'opacity .15s',
|
||||
pointerEvents: active ? 'auto' : 'none',
|
||||
opacity: active ? 1 : 0,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: active ? 0 : '100%',
|
||||
bottom: 0,
|
||||
zIndex: parseInt(baseTheme.zIndexModal) - 1,
|
||||
background: 'var(--affine-background-modal-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledScrollWrapper = styled('div')<{
|
||||
showTopBorder: boolean;
|
||||
}>(({ showTopBorder }) => {
|
||||
return {
|
||||
maxHeight: '50%',
|
||||
overflowY: 'auto',
|
||||
borderTop: '1px solid',
|
||||
borderColor: showTopBorder ? 'var(--affine-border-color)' : 'transparent',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { RadioButton, RadioButtonGroup } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
|
||||
import {
|
||||
allPageModeSelectAtom,
|
||||
openQuickSearchModalAtom,
|
||||
} from '../../../atoms';
|
||||
import type { HeaderProps } from '../../blocksuite/workspace-header/header';
|
||||
import { Header } from '../../blocksuite/workspace-header/header';
|
||||
import * as styles from '../../blocksuite/workspace-header/styles.css';
|
||||
import { QuickSearchButton } from '../quick-search-button';
|
||||
|
||||
export type WorkspaceTitleProps = React.PropsWithChildren<
|
||||
HeaderProps & {
|
||||
icon?: ReactNode;
|
||||
}
|
||||
>;
|
||||
|
||||
export const WorkspaceTitle: React.FC<WorkspaceTitleProps> = ({
|
||||
icon,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
|
||||
return (
|
||||
<Header {...props}>
|
||||
<div className={styles.pageListTitleWrapper}>
|
||||
<div className={styles.pageListTitleIcon}>{icon}</div>
|
||||
{children}
|
||||
<QuickSearchButton
|
||||
onClick={() => {
|
||||
setOpenQuickSearch(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceModeFilterTab = ({ ...props }: WorkspaceTitleProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [value, setMode] = useAtom(allPageModeSelectAtom);
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value !== 'all' && value !== 'page' && value !== 'edgeless') {
|
||||
throw new Error('Invalid value for page mode option');
|
||||
}
|
||||
setMode(value);
|
||||
};
|
||||
return (
|
||||
<Header {...props}>
|
||||
<div className={styles.allPageListTitleWrapper}>
|
||||
<RadioButtonGroup
|
||||
width={300}
|
||||
defaultValue={value}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<RadioButton value="all" style={{ textTransform: 'capitalize' }}>
|
||||
{t['all']()}
|
||||
</RadioButton>
|
||||
<RadioButton value="page">{t['Page']()}</RadioButton>
|
||||
<RadioButton value="edgeless">{t['Edgeless']()}</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user