mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: implement drag and drop for resource order in editor (#3337)
* Implement drag and drop for resource order in editor * chore: update * chore: update * chore: update
This commit is contained in:
@ -75,7 +75,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
|||||||
fields = append(fields, "`blob`")
|
fields = append(fields, "`blob`")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `created_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND "))
|
query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `updated_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||||
if find.Limit != nil {
|
if find.Limit != nil {
|
||||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||||
if find.Offset != nil {
|
if find.Offset != nil {
|
||||||
|
@ -71,7 +71,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
|||||||
%s
|
%s
|
||||||
FROM resource
|
FROM resource
|
||||||
WHERE %s
|
WHERE %s
|
||||||
ORDER BY created_ts DESC
|
ORDER BY updated_ts DESC
|
||||||
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||||
if find.Limit != nil {
|
if find.Limit != nil {
|
||||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||||
|
@ -68,7 +68,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
|||||||
fields = append(fields, "`blob`")
|
fields = append(fields, "`blob`")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `created_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND "))
|
query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `updated_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||||
if find.Limit != nil {
|
if find.Limit != nil {
|
||||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||||
if find.Offset != nil {
|
if find.Offset != nil {
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
"postinstall": "cd ../proto && buf generate"
|
"postinstall": "cd ../proto && buf generate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.5",
|
"@emotion/styled": "^11.11.5",
|
||||||
"@github/relative-time-element": "^4.4.0",
|
"@github/relative-time-element": "^4.4.0",
|
||||||
|
56
web/pnpm-lock.yaml
generated
56
web/pnpm-lock.yaml
generated
@ -8,6 +8,15 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@dnd-kit/core':
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/sortable':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/utilities':
|
||||||
|
specifier: ^3.2.2
|
||||||
|
version: 3.2.2(react@18.3.1)
|
||||||
'@emotion/react':
|
'@emotion/react':
|
||||||
specifier: ^11.11.4
|
specifier: ^11.11.4
|
||||||
version: 11.11.4(@types/react@18.3.1)(react@18.3.1)
|
version: 11.11.4(@types/react@18.3.1)(react@18.3.1)
|
||||||
@ -348,6 +357,28 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@dnd-kit/accessibility@3.1.0':
|
||||||
|
resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
|
'@dnd-kit/core@6.1.0':
|
||||||
|
resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
|
'@dnd-kit/sortable@8.0.0':
|
||||||
|
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@dnd-kit/core': ^6.1.0
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
|
'@dnd-kit/utilities@3.2.2':
|
||||||
|
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
'@emotion/babel-plugin@11.11.0':
|
'@emotion/babel-plugin@11.11.0':
|
||||||
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
|
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
|
||||||
|
|
||||||
@ -3234,6 +3265,31 @@ snapshots:
|
|||||||
'@bufbuild/buf-win32-arm64': 1.31.0
|
'@bufbuild/buf-win32-arm64': 1.31.0
|
||||||
'@bufbuild/buf-win32-x64': 1.31.0
|
'@bufbuild/buf-win32-x64': 1.31.0
|
||||||
|
|
||||||
|
'@dnd-kit/accessibility@3.1.0(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
'@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@dnd-kit/accessibility': 3.1.0(react@18.3.1)
|
||||||
|
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
'@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@dnd-kit/core': 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
tslib: 2.6.2
|
||||||
|
|
||||||
'@emotion/babel-plugin@11.11.0':
|
'@emotion/babel-plugin@11.11.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-module-imports': 7.24.3
|
'@babel/helper-module-imports': 7.24.3
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core";
|
||||||
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
import { Resource } from "@/types/proto/api/v1/resource_service";
|
import { Resource } from "@/types/proto/api/v1/resource_service";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import ResourceIcon from "../ResourceIcon";
|
import ResourceIcon from "../ResourceIcon";
|
||||||
|
import SortableItem from "./SortableItem";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resourceList: Resource[];
|
resourceList: Resource[];
|
||||||
@ -9,33 +12,48 @@ interface Props {
|
|||||||
|
|
||||||
const ResourceListView = (props: Props) => {
|
const ResourceListView = (props: Props) => {
|
||||||
const { resourceList, setResourceList } = props;
|
const { resourceList, setResourceList } = props;
|
||||||
|
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
||||||
|
|
||||||
const handleDeleteResource = async (name: string) => {
|
const handleDeleteResource = async (name: string) => {
|
||||||
setResourceList(resourceList.filter((resource) => resource.name !== name));
|
setResourceList(resourceList.filter((resource) => resource.name !== name));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
const oldIndex = resourceList.findIndex((resource) => resource.name === active.id);
|
||||||
|
const newIndex = resourceList.findIndex((resource) => resource.name === over.id);
|
||||||
|
|
||||||
|
setResourceList(arrayMove(resourceList, oldIndex, newIndex));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
{resourceList.length > 0 && (
|
<SortableContext items={resourceList.map((resource) => resource.name)} strategy={verticalListSortingStrategy}>
|
||||||
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2">
|
{resourceList.length > 0 && (
|
||||||
{resourceList.map((resource) => {
|
<div className="w-full flex flex-row justify-start flex-wrap gap-2 mt-2">
|
||||||
return (
|
{resourceList.map((resource) => {
|
||||||
<div
|
return (
|
||||||
key={resource.name}
|
<SortableItem
|
||||||
className="max-w-full flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded text-gray-500 dark:text-gray-400"
|
key={resource.name}
|
||||||
>
|
id={resource.name}
|
||||||
<ResourceIcon resource={resource} className="!w-4 !h-4 !opacity-100" />
|
className="max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400"
|
||||||
<span className="text-sm max-w-[8rem] truncate">{resource.filename}</span>
|
>
|
||||||
<Icon.X
|
<ResourceIcon resource={resource} className="!w-4 !h-4 !opacity-100" />
|
||||||
className="w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"
|
<span className="text-sm max-w-[8rem] truncate">{resource.filename}</span>
|
||||||
onClick={() => handleDeleteResource(resource.name)}
|
<Icon.X
|
||||||
/>
|
className="w-4 h-auto cursor-pointer opacity-60 hover:opacity-100"
|
||||||
</div>
|
onClick={() => handleDeleteResource(resource.name)}
|
||||||
);
|
/>
|
||||||
})}
|
</SortableItem>
|
||||||
</div>
|
);
|
||||||
)}
|
})}
|
||||||
</>
|
</div>
|
||||||
|
)}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
25
web/src/components/MemoEditor/SortableItem.tsx
Normal file
25
web/src/components/MemoEditor/SortableItem.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
className: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableItem: React.FC<Props> = ({ id, className, children }: Props) => {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners} className={className}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableItem;
|
Reference in New Issue
Block a user