diff --git a/apps/browser-extension/package-lock.json b/apps/browser-extension/package-lock.json index c96691cf3..3a7dd965d 100644 --- a/apps/browser-extension/package-lock.json +++ b/apps/browser-extension/package-lock.json @@ -1,14 +1,17 @@ { "name": "aliasvault-browser-extension", - "version": "0.26.0", + "version": "0.27.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aliasvault-browser-extension", - "version": "0.26.0", + "version": "0.27.0", "hasInstallScript": true, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.1.1", "@types/dompurify": "^3.0.5", "argon2-browser": "^1.18.0", @@ -710,6 +713,59 @@ } } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -11803,7 +11859,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tunnel-agent": { diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index b265df2a5..d3e648b5f 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -29,6 +29,9 @@ "postinstall": "wxt prepare" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.1.1", "@types/dompurify": "^3.0.5", "argon2-browser": "^1.18.0", diff --git a/apps/browser-extension/src/entrypoints/popup/components/Forms/DraggableCustomFieldsList.tsx b/apps/browser-extension/src/entrypoints/popup/components/Forms/DraggableCustomFieldsList.tsx new file mode 100644 index 000000000..745b83998 --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/components/Forms/DraggableCustomFieldsList.tsx @@ -0,0 +1,220 @@ +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { FieldType } from '@/utils/dist/core/models/vault'; +import { FieldTypes } from '@/utils/dist/core/models/vault'; + +import EditableFieldLabel from './EditableFieldLabel'; +import { FormInput } from './FormInput'; +import HiddenField from './HiddenField'; + +/** + * Custom field definition type + */ +export type CustomFieldDefinition = { + tempId: string; + label: string; + fieldType: FieldType; + isHidden: boolean; + displayOrder: number; +}; + +interface ISortableCustomFieldProps { + field: CustomFieldDefinition; + value: string; + onValueChange: (value: string) => void; + onLabelChange: (newLabel: string) => void; + onDelete: () => void; +} + +/** + * Individual sortable custom field item + */ +const SortableCustomField: React.FC = ({ + field, + value, + onValueChange, + onLabelChange, + onDelete, +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: field.tempId }); + + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + zIndex: isDragging ? 1000 : 'auto', + }; + + /** + * Renders the appropriate input field based on field type + */ + const renderFieldInput = (): React.ReactNode => { + if (field.fieldType === FieldTypes.TextArea) { + return ( +