mirror of
https://github.com/AppFlowy-IO/AppFlowy
synced 2026-05-23 17:18:31 +00:00
fix: some duplicate issues (#6171)
This commit is contained in:
parent
4b0368d552
commit
94c033bba8
8 changed files with 141 additions and 84 deletions
|
|
@ -212,7 +212,6 @@ export class AFClientService implements AFService {
|
|||
async getCurrentUser () {
|
||||
const data = await APIService.getCurrentUser();
|
||||
|
||||
await APIService.getWorkspaces();
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { notify } from '@/components/_shared/notify';
|
||||
import { loadEmojiData } from '@/utils/emoji';
|
||||
import { EmojiMartData } from '@emoji-mart/data';
|
||||
import { PopoverProps } from '@mui/material/Popover';
|
||||
|
|
@ -16,8 +17,10 @@ interface Emoji {
|
|||
native: string;
|
||||
}
|
||||
|
||||
export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: string) => void }) {
|
||||
export function useLoadEmojiData ({ onEmojiSelect }: { onEmojiSelect: (emoji: string) => void }) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const [emojiCategories, setEmojiCategories] = useState<EmojiCategory[]>([]);
|
||||
const [skin, setSkin] = useState<number>(() => {
|
||||
return Number(localStorage.getItem('emoji-mart.skin')) || 0;
|
||||
|
|
@ -30,40 +33,52 @@ export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: str
|
|||
|
||||
const searchEmojiData = useCallback(
|
||||
async (searchVal?: string) => {
|
||||
const emojiData = await loadEmojiData();
|
||||
setLoading(true);
|
||||
setIsEmpty(false);
|
||||
try {
|
||||
const emojiData = await loadEmojiData();
|
||||
const { emojis, categories } = emojiData as EmojiMartData;
|
||||
|
||||
const { emojis, categories } = emojiData as EmojiMartData;
|
||||
const filteredCategories = categories
|
||||
.map((category) => {
|
||||
const { id, emojis: categoryEmojis } = category;
|
||||
|
||||
const filteredCategories = categories
|
||||
.map((category) => {
|
||||
const { id, emojis: categoryEmojis } = category;
|
||||
return {
|
||||
id,
|
||||
emojis: categoryEmojis
|
||||
.filter((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
|
||||
return {
|
||||
id,
|
||||
emojis: categoryEmojis
|
||||
.filter((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
if (!searchVal) return true;
|
||||
return filterSearchValue(emoji, searchVal);
|
||||
})
|
||||
.map((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
const { name, skins } = emoji;
|
||||
|
||||
if (!searchVal) return true;
|
||||
return filterSearchValue(emoji, searchVal);
|
||||
})
|
||||
.map((emojiId) => {
|
||||
const emoji = emojis[emojiId];
|
||||
const { name, skins } = emoji;
|
||||
return {
|
||||
id: emojiId,
|
||||
name,
|
||||
native: skins[skin] ? skins[skin].native : skins[0].native,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter((category) => category.emojis.length > 0);
|
||||
|
||||
return {
|
||||
id: emojiId,
|
||||
name,
|
||||
native: skins[skin] ? skins[skin].native : skins[0].native,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter((category) => category.emojis.length > 0);
|
||||
if (filteredCategories.length === 0) {
|
||||
setIsEmpty(true);
|
||||
}
|
||||
|
||||
setEmojiCategories(filteredCategories);
|
||||
setEmojiCategories(filteredCategories);
|
||||
} catch (_e) {
|
||||
notify.error('Failed to load emoji data');
|
||||
setIsEmpty(true);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
[skin]
|
||||
[skin],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -80,7 +95,7 @@ export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: str
|
|||
async (native: string) => {
|
||||
onEmojiSelect(native);
|
||||
},
|
||||
[onEmojiSelect]
|
||||
[onEmojiSelect],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -90,10 +105,12 @@ export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: str
|
|||
onSelect,
|
||||
onSkinChange,
|
||||
skin,
|
||||
loading,
|
||||
isEmpty,
|
||||
};
|
||||
}
|
||||
|
||||
export function useSelectSkinPopoverProps(): PopoverProps & {
|
||||
export function useSelectSkinPopoverProps (): PopoverProps & {
|
||||
onOpen: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onClose: () => void;
|
||||
} {
|
||||
|
|
@ -118,12 +135,12 @@ export function useSelectSkinPopoverProps(): PopoverProps & {
|
|||
};
|
||||
}
|
||||
|
||||
function filterSearchValue(
|
||||
function filterSearchValue (
|
||||
emoji: {
|
||||
name: string;
|
||||
keywords?: string[];
|
||||
},
|
||||
searchValue: string
|
||||
searchValue: string,
|
||||
) {
|
||||
const { name, keywords } = emoji;
|
||||
const searchValueLowerCase = searchValue.toLowerCase();
|
||||
|
|
@ -134,7 +151,7 @@ function filterSearchValue(
|
|||
);
|
||||
}
|
||||
|
||||
export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize: number) {
|
||||
export function getRowsWithCategories (emojiCategories: EmojiCategory[], rowSize: number) {
|
||||
const rows: {
|
||||
id: string;
|
||||
type: 'category' | 'emojis';
|
||||
|
|
@ -157,4 +174,4 @@ export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize:
|
|||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import React from 'react';
|
||||
|
||||
import { useLoadEmojiData } from './EmojiPicker.hooks';
|
||||
import EmojiPickerHeader from './EmojiPickerHeader';
|
||||
import EmojiPickerCategories from './EmojiPickerCategories';
|
||||
import emptyImageSrc from '@/assets/images/empty.png';
|
||||
|
||||
interface Props {
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
|
|
@ -11,8 +13,9 @@ interface Props {
|
|||
hideRemove?: boolean;
|
||||
}
|
||||
|
||||
export function EmojiPicker({ defaultEmoji, onEscape, ...props }: Props) {
|
||||
const { skin, onSkinChange, emojiCategories, setSearchValue, searchValue, onSelect } = useLoadEmojiData(props);
|
||||
export function EmojiPicker ({ defaultEmoji, onEscape, ...props }: Props) {
|
||||
const { skin, onSkinChange, emojiCategories, setSearchValue, searchValue, onSelect, loading, isEmpty } =
|
||||
useLoadEmojiData(props);
|
||||
|
||||
return (
|
||||
<div tabIndex={0} className={'emoji-picker flex h-[360px] max-h-[70vh] flex-col p-4 pt-2'}>
|
||||
|
|
@ -24,14 +27,22 @@ export function EmojiPicker({ defaultEmoji, onEscape, ...props }: Props) {
|
|||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
/>
|
||||
<EmojiPickerCategories
|
||||
defaultEmoji={defaultEmoji}
|
||||
onEscape={onEscape}
|
||||
onEmojiSelect={onSelect}
|
||||
emojiCategories={emojiCategories}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className={'flex h-full items-center justify-center'}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<img src={emptyImageSrc} alt={'No data found'} className={'mx-auto h-[200px]'} />
|
||||
) : (
|
||||
<EmojiPickerCategories
|
||||
defaultEmoji={defaultEmoji}
|
||||
onEscape={onEscape}
|
||||
onEmojiSelect={onSelect}
|
||||
emojiCategories={emojiCategories}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmojiPicker;
|
||||
export default EmojiPicker;
|
||||
|
|
@ -13,6 +13,8 @@ import { useTranslation } from 'react-i18next';
|
|||
import { ReactComponent as CloseIcon } from '@/assets/close.svg';
|
||||
import { ReactComponent as DeleteIcon } from '@/assets/trash.svg';
|
||||
import './template.scss';
|
||||
import { slugify } from '@/components/as-template/utils';
|
||||
import { ReactComponent as WebsiteIcon } from '@/assets/website.svg';
|
||||
|
||||
function AsTemplate ({
|
||||
viewName,
|
||||
|
|
@ -56,9 +58,10 @@ function AsTemplate ({
|
|||
await service?.updateTemplate(template.view_id, formData);
|
||||
} else {
|
||||
await service?.createTemplate(formData);
|
||||
await loadTemplate();
|
||||
|
||||
}
|
||||
|
||||
await loadTemplate();
|
||||
handleBack();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
|
|
@ -108,6 +111,15 @@ function AsTemplate ({
|
|||
>
|
||||
{t('button.cancel')}
|
||||
</Button>
|
||||
{template && <Button
|
||||
startIcon={<WebsiteIcon />}
|
||||
variant={'text'}
|
||||
onClick={() => {
|
||||
const url = import.meta.env.AF_BASE_URL?.includes('test') ? 'https://test.appflowy.io' : 'https://appflowy.io';
|
||||
|
||||
window.open(`${url}/templates/${slugify(template.categories[0].name)}/${template.view_id}`);
|
||||
}} color={'primary'}
|
||||
>{t('template.viewTemplate')}</Button>}
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{template && <Button
|
||||
startIcon={<DeleteIcon />}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
export function slugify (text: string) {
|
||||
return text
|
||||
.toString() // ensure the text is a string
|
||||
.toLowerCase() // make the text lowercase
|
||||
.trim() // remove leading and trailing whitespaces
|
||||
.replace(/\s+/g, '-') // replace all whitespaces with '-'
|
||||
.replace(/[^\w-]+/g, '') // remove all non-word characters
|
||||
.replace(/--+/g, '-'); // replace multiple '-' with single '-'
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import { useDuplicate } from '@/components/publish/header/duplicate/useDuplicate';
|
||||
import DuplicateModal from '@/components/publish/header/duplicate/DuplicateModal';
|
||||
|
||||
export function Duplicate() {
|
||||
export function Duplicate () {
|
||||
const { t } = useTranslation();
|
||||
const { loginOpen, duplicateOpen, handleDuplicateClose, handleLoginClose, url } = useDuplicate();
|
||||
const [, setSearch] = useSearchParams();
|
||||
|
|
@ -32,10 +32,10 @@ export function Duplicate() {
|
|||
open={loginOpen}
|
||||
onClose={handleLoginClose}
|
||||
/>
|
||||
{duplicateOpen && <DuplicateModal
|
||||
<DuplicateModal
|
||||
open={duplicateOpen}
|
||||
onClose={handleDuplicateClose}
|
||||
/>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { PublishContext } from '@/application/publish';
|
|||
import { CollabType, ViewLayout } from '@/application/collab.type';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
function getCollabTypeFromViewLayout(layout: ViewLayout) {
|
||||
function getCollabTypeFromViewLayout (layout: ViewLayout) {
|
||||
switch (layout) {
|
||||
case ViewLayout.Document:
|
||||
return CollabType.Document;
|
||||
|
|
@ -23,7 +23,7 @@ function getCollabTypeFromViewLayout(layout: ViewLayout) {
|
|||
}
|
||||
}
|
||||
|
||||
function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
function DuplicateModal ({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const service = useContext(AFConfigContext)?.service;
|
||||
const viewMeta = useContext(PublishContext)?.viewMeta;
|
||||
|
|
@ -40,6 +40,8 @@ function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void
|
|||
selectedSpaceId,
|
||||
workspaceLoading,
|
||||
spaceLoading,
|
||||
loadWorkspaces,
|
||||
loadSpaces,
|
||||
} = useLoadWorkspaces();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -49,6 +51,18 @@ function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void
|
|||
}
|
||||
}, [open, setSelectedSpaceId, setSelectedWorkspaceId, workspaceList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
void loadWorkspaces();
|
||||
}
|
||||
}, [loadWorkspaces, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWorkspaceId) {
|
||||
void loadSpaces(selectedWorkspaceId);
|
||||
}
|
||||
}, [loadSpaces, selectedWorkspaceId]);
|
||||
|
||||
const handleDuplicate = useCallback(async () => {
|
||||
if (!viewId) return;
|
||||
const collabType = getCollabTypeFromViewLayout(layout);
|
||||
|
|
@ -126,4 +140,4 @@ function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void
|
|||
);
|
||||
}
|
||||
|
||||
export default DuplicateModal;
|
||||
export default DuplicateModal;
|
||||
|
|
@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import { SpaceView, Workspace } from '@/application/types';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
export function useDuplicate() {
|
||||
export function useDuplicate () {
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const [loginOpen, setLoginOpen] = React.useState(false);
|
||||
|
|
@ -46,11 +46,11 @@ export function useDuplicate() {
|
|||
};
|
||||
}
|
||||
|
||||
export function useLoadWorkspaces() {
|
||||
export function useLoadWorkspaces () {
|
||||
const [spaceLoading, setSpaceLoading] = useState<boolean>(false);
|
||||
const [workspaceLoading, setWorkspaceLoading] = useState<boolean>(false);
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>('1');
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>('1');
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>('');
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>('');
|
||||
|
||||
const [workspaceList, setWorkspaceList] = useState<Workspace[]>([]);
|
||||
|
||||
|
|
@ -58,36 +58,28 @@ export function useLoadWorkspaces() {
|
|||
|
||||
const service = useContext(AFConfigContext)?.service;
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
setWorkspaceLoading(true);
|
||||
try {
|
||||
const workspaces = await service?.getWorkspaces();
|
||||
const loadWorkspaces = useCallback(async () => {
|
||||
setWorkspaceLoading(true);
|
||||
try {
|
||||
const workspaces = await service?.getWorkspaces();
|
||||
|
||||
if (workspaces) {
|
||||
setWorkspaceList(workspaces);
|
||||
setSelectedWorkspaceId(workspaces[0].id);
|
||||
} else {
|
||||
setWorkspaceList([]);
|
||||
setSelectedWorkspaceId('');
|
||||
}
|
||||
} catch (e) {
|
||||
notify.error('Failed to load workspaces');
|
||||
} finally {
|
||||
setWorkspaceLoading(false);
|
||||
if (workspaces) {
|
||||
setWorkspaceList(workspaces);
|
||||
setSelectedWorkspaceId(workspaces[0].id);
|
||||
} else {
|
||||
setWorkspaceList([]);
|
||||
setSelectedWorkspaceId('');
|
||||
}
|
||||
})();
|
||||
} catch (e) {
|
||||
notify.error('Failed to load workspaces');
|
||||
} finally {
|
||||
setWorkspaceLoading(false);
|
||||
}
|
||||
}, [service]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceList.length === 0 || !selectedWorkspaceId || workspaceLoading) {
|
||||
setSpaceList([]);
|
||||
setSelectedSpaceId('');
|
||||
return;
|
||||
}
|
||||
|
||||
setSpaceLoading(true);
|
||||
void (async () => {
|
||||
const loadSpaces = useCallback(
|
||||
async (selectedWorkspaceId: string) => {
|
||||
setSpaceLoading(true);
|
||||
try {
|
||||
const folder = await service?.getWorkspaceFolder(selectedWorkspaceId);
|
||||
|
||||
|
|
@ -115,8 +107,9 @@ export function useLoadWorkspaces() {
|
|||
setSelectedSpaceId('');
|
||||
setSpaceLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [selectedWorkspaceId, service, workspaceList.length, workspaceLoading]);
|
||||
},
|
||||
[service],
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceList,
|
||||
|
|
@ -127,5 +120,7 @@ export function useLoadWorkspaces() {
|
|||
setSelectedSpaceId,
|
||||
workspaceLoading,
|
||||
spaceLoading,
|
||||
loadWorkspaces,
|
||||
loadSpaces,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue