diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..d772b85 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/components/ActionDropdown.tsx b/components/ActionDropdown.tsx index 8c4608b..862bfac 100644 --- a/components/ActionDropdown.tsx +++ b/components/ActionDropdown.tsx @@ -1,5 +1,4 @@ "use client"; - import { Dialog, DialogContent, @@ -7,6 +6,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; + import { DropdownMenu, DropdownMenuContent, @@ -15,11 +15,11 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { useState } from "react"; +import React, { useState } from "react"; import Image from "next/image"; +import Link from "next/link"; import { Models } from "node-appwrite"; import { actionsDropdownItems } from "@/constants"; -import Link from "next/link"; import { constructDownloadUrl } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -29,24 +29,34 @@ import { updateFileUsers, } from "@/lib/actions/file.actions"; import { usePathname } from "next/navigation"; +import { useToast } from "@/hooks/use-toast"; import { FileDetails, ShareInput } from "@/components/ActionsModalContent"; -const ActionDropdown = ({ file }: { file: Models.Document }) => { +const ActionDropDown = ({ + file, + currentUserEmail, +}: { + file: Models.Document; + currentUserEmail: string; +}) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [action, setAction] = useState(null); const [name, setName] = useState(file.name); const [isLoading, setIsLoading] = useState(false); const [emails, setEmails] = useState([]); + const [isAdmin, setIsAdmin] = useState(false); const path = usePathname(); + const { toast } = useToast(); + const { users: currentUsers, AdminUsers: currentAdminUsers } = file; // extracting user and admin emails const closeAllModals = () => { setIsModalOpen(false); setIsDropdownOpen(false); setAction(null); setName(file.name); - // setEmails([]); + // setEmails([]); }; const handleAction = async () => { @@ -54,32 +64,80 @@ const ActionDropdown = ({ file }: { file: Models.Document }) => { setIsLoading(true); let success = false; + // Update the respective list based on admin status + const updatedUserEmails = isAdmin + ? currentUsers + : Array.from(new Set([...currentUsers, ...emails])); + + const updatedAdminEmails = isAdmin + ? Array.from(new Set([...currentAdminUsers, ...emails])) + : currentAdminUsers; + const actions = { rename: () => renameFile({ fileId: file.$id, name, extension: file.extension, path }), - share: () => updateFileUsers({ fileId: file.$id, emails, path }), + + share: () => + updateFileUsers({ + fileId: file.$id, + userEmails: updatedUserEmails, + adminEmails: updatedAdminEmails, + path, + }), + delete: () => - deleteFile({ fileId: file.$id, bucketFileId: file.bucketFileId, path }), + deleteFile({ + fileId: file.$id, + bucketFileId: file.bucketFileId, + path, + }), }; success = await actions[action.value as keyof typeof actions](); - if (success) closeAllModals(); - + if (success) { + closeAllModals(); + toast({ + description: ( +

{`${action.label} successfully done`}

+ ), + className: "success-toast", + }); + } + setIsAdmin(false); setIsLoading(false); }; const handleRemoveUser = async (email: string) => { - const updatedEmails = emails.filter((e) => e !== email); + try { + // Extract current user and admin email lists - const success = await updateFileUsers({ - fileId: file.$id, - emails: updatedEmails, - path, - }); + // Determine the updated user and admin email lists + const updatedAdminEmails = currentAdminUsers.includes(email) + ? currentAdminUsers.filter((e: string) => e !== email) + : currentAdminUsers; - if (success) setEmails(updatedEmails); - closeAllModals(); + const updatedUserEmails = currentUsers.includes(email) + ? currentUsers.filter((e: string) => e !== email) + : currentUsers; + + // Update the file's user information + const isUpdated = await updateFileUsers({ + fileId: file.$id, + userEmails: updatedUserEmails, + adminEmails: updatedAdminEmails, + path, + }); + + // Update the local state if the update was successful + if (isUpdated) { + setEmails((prevEmails) => prevEmails.filter((e) => e !== email)); + // closeAllModals(); Uncomment if modal handling is needed + } + } catch (error) { + console.error("Failed to remove user:", error); + // Add any user-facing error handling or reporting logic here + } }; const renderDialogContent = () => { @@ -88,9 +146,9 @@ const ActionDropdown = ({ file }: { file: Models.Document }) => { const { value, label } = action; return ( - - - + + + {label} {value === "rename" && ( @@ -100,35 +158,41 @@ const ActionDropdown = ({ file }: { file: Models.Document }) => { onChange={(e) => setName(e.target.value)} /> )} + {value === "details" && } + {value === "share" && ( )} + {value === "delete" && ( -

- Are you sure you want to delete{` `} - {file.name}? +

+ Are you sure you want to delete{" "} + {file.name}?

)}
{["rename", "delete", "share"].includes(value) && ( - - - @@ -141,62 +205,71 @@ const ActionDropdown = ({ file }: { file: Models.Document }) => { return ( - + dots - + {file.name} - {actionsDropdownItems.map((actionItem) => ( - { - setAction(actionItem); - - if ( - ["rename", "share", "delete", "details"].includes( - actionItem.value, - ) - ) { - setIsModalOpen(true); - } - }} - > - {actionItem.value === "download" ? ( - - {actionItem.label} - {actionItem.label} - - ) : ( -
- {actionItem.label} - {actionItem.label} -
- )} -
- ))} + {actionsDropdownItems + .filter( + (actionItem) => + (actionItem.value !== "delete" && + actionItem.value !== "rename") || + file.owner.email === currentUserEmail || + currentAdminUsers.includes(currentUserEmail), + // TODO: check if the currentUser has admin privileges + ) + .map((actionItem) => ( + { + setAction(actionItem); + + if ( + ["rename", "share", "delete", "details"].includes( + actionItem.value, + ) + ) { + setIsModalOpen(true); + } + }} + > + {actionItem.value === "download" ? ( + + {actionItem.label} + {actionItem.label} + + ) : ( +
+ {actionItem.label} + {actionItem.label} +
+ )} +
+ ))}
@@ -204,4 +277,4 @@ const ActionDropdown = ({ file }: { file: Models.Document }) => {
); }; -export default ActionDropdown; +export default ActionDropDown; diff --git a/components/ActionsModalContent.tsx b/components/ActionsModalContent.tsx index 6954a15..a536d28 100644 --- a/components/ActionsModalContent.tsx +++ b/components/ActionsModalContent.tsx @@ -1,96 +1,161 @@ +// TODO: check if the currentUser has admin privileges + import { Models } from "node-appwrite"; import Thumbnail from "@/components/Thumbnail"; import FormattedDateTime from "@/components/FormattedDateTime"; import { convertFileSize, formatDateTime } from "@/lib/utils"; -import React from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import Image from "next/image"; +import { Switch } from "@/components/ui/switch"; const ImageThumbnail = ({ file }: { file: Models.Document }) => ( -
- -
-

{file.name}

- +
+ + +
+

{file.name}

+ +
-
); const DetailRow = ({ label, value }: { label: string; value: string }) => ( -
-

{label}

-

{value}

-
+
+

{label}

+

{value}

+
); - export const FileDetails = ({ file }: { file: Models.Document }) => { - return ( - <> - -
- - - - -
- - ); + return ( + <> + +
+ + + + +
+ + ); }; interface Props { - file: Models.Document; - onInputChange: React.Dispatch>; - onRemove: (email: string) => void; + file: Models.Document; + onInputChange: React.Dispatch>; + onRemove: (email: string) => void; + currentUserEmail: string; + setIsAdmin: React.Dispatch>; } -export const ShareInput = ({ file, onInputChange, onRemove }: Props) => { - return ( - <> - +export const ShareInput = ({ + file, + onInputChange, + onRemove, + currentUserEmail, + setIsAdmin, + }: Props) => { + // console.log(isAdmin); + const totalSharedUser = file.users.length + file.AdminUsers.length; + return ( + <> + -
-

- Share file with other users -

- onInputChange(e.target.value.trim().split(","))} - className="share-input-field" - /> -
-
-

Shared with

-

- {file.users.length} users -

-
+
+

+ Share file with other user +

+ { + const inputEmails = e.target.value.trim().split(","); + onInputChange(inputEmails); + }} + className="share-input-field" + /> -
    - {file.users.map((email: string) => ( -
  • -

    {email}

    - -
  • - ))} -
-
-
- - ); + {file.owner.email === currentUserEmail && ( +
+ setIsAdmin(checked)} + className={"data-[state=checked]:bg-red"} + /> +
+

+ Provide Admin Privileges +

+

+ This includes allowing the user to rename, delete, and share + files, ensuring that shared users do not receive admin + privileges. +

+
+
+ )} + +
+
+

Shared with

+

+ {totalSharedUser} users +

+
+ +
    + {file.AdminUsers.map((email: string) => ( +
  • +

    {email}

    +

    Admin

    + + {currentUserEmail === file.owner.email && ( + + )} +
  • + ))} + {file.users.map((email: string) => ( +
  • +

    {email}

    + + {currentUserEmail === file.owner.email && ( // TODO: check if current user is in admin email array + + )} +
  • + ))} +
+
+
+ + ); }; diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/lib/actions/file.actions.ts b/lib/actions/file.actions.ts index 765030d..008e432 100644 --- a/lib/actions/file.actions.ts +++ b/lib/actions/file.actions.ts @@ -72,6 +72,7 @@ const createQueries = ( Query.or([ Query.equal("owner", [currentUser.$id]), Query.contains("users", [currentUser.email]), + Query.contains("AdminUsers", [currentUser.email]), ]), ]; @@ -111,7 +112,7 @@ export const getFiles = async ({ queries, ); - console.log({ files }); + // console.log({ files }); return parseStringify(files); } catch (error) { handleError(error, "Failed to get files"); @@ -146,7 +147,8 @@ export const renameFile = async ({ export const updateFileUsers = async ({ fileId, - emails, + userEmails, + adminEmails, path, }: UpdateFileUsersProps) => { const { databases } = await createAdminClient(); @@ -157,7 +159,8 @@ export const updateFileUsers = async ({ appwriteConfig.filesCollectionId, fileId, { - users: emails, + AdminUsers: adminEmails, + users: userEmails, }, ); diff --git a/package-lock.json b/package-lock.json index 16de3b2..2adeda3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -1515,6 +1516,97 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", diff --git a/package.json b/package.json index 71fc713..b6d90c7 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-toast": "^1.2.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/types/index.d.ts b/types/index.d.ts index 55435ef..9f7eeaf 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -33,9 +33,11 @@ declare interface RenameFileProps { } declare interface UpdateFileUsersProps { fileId: string; - emails: string[]; + userEmails: string[]; + adminEmails: string[]; path: string; } + declare interface DeleteFileProps { fileId: string; bucketFileId: string;