From 26f1ef7ccb1155450df62c0838a5ce22aee29fa7 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 16:41:25 +0000 Subject: [PATCH 01/16] Added User search field component Signed-off-by: Yash Sharma --- .../UserSearchField/UserSearchField.tsx | 237 ++++++++++++++++++ src/custom/UserSearchField/index.tsx | 3 + src/custom/index.tsx | 1 + src/icons/Person/PersonIcon.tsx | 17 ++ src/icons/Person/index.ts | 1 + 5 files changed, 259 insertions(+) create mode 100644 src/custom/UserSearchField/UserSearchField.tsx create mode 100644 src/custom/UserSearchField/index.tsx create mode 100644 src/icons/Person/PersonIcon.tsx create mode 100644 src/icons/Person/index.ts diff --git a/src/custom/UserSearchField/UserSearchField.tsx b/src/custom/UserSearchField/UserSearchField.tsx new file mode 100644 index 00000000..e782a094 --- /dev/null +++ b/src/custom/UserSearchField/UserSearchField.tsx @@ -0,0 +1,237 @@ +import React, { useState } from "react"; +import { PersonIcon } from "../../icons/Person"; +import { iconSmall } from '../../constants/iconsSizes'; +import { CloseIcon } from "../../icons/Close"; +import { Box, Chip, Grid, TextField, Tooltip, Typography, Avatar } from "../../base" +import Autocomplete from "@mui/material/Autocomplete"; +import CircularProgress from "@mui/material/CircularProgress"; +import { useTheme } from "../../theme" + +interface User { + user_id: string; + first_name: string; + last_name: string; + email: string; + avatar_url?: string; + deleted_at?: { Valid: boolean }; +} + +interface UserSearchFieldProps { + // Array of user objects currently selected. + usersData: User[]; + // Function to update the selected users data. + setUsersData: (users: User[]) => void; + // Label for the text field. + label?: string; + // Function to enable or disable the save button. + setDisableSave?: (disabled: boolean) => void; + // Type of search being performed, e.g., 'user', 'admin'. + searchType?: string; + // Boolean indicating whether the search field is disabled. + disabled?: boolean; + // Custom component to change rendering style of users list, if not given + // by default it will show list with avatar and email of selected users + customUsersList?: JSX.Element; + /** + * Function to fetch user suggestions based on the input value. + * @param {string} value - The input value for which suggestions are to be fetched. + * @returns {Promise} A promise that resolves to an array of user suggestions. + */ + fetchSuggestions: (value: string) => Promise; +} + +const UserSearchField: React.FC = ({ + usersData, + setUsersData, + label, + setDisableSave, + disabled = false, + customUsersList, + fetchSuggestions +}: UserSearchFieldProps) => { + const [error, setError] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [options, setOptions] = useState([]); + const [open, setOpen] = useState(false); + const [searchUserLoading, setSearchUserLoading] = useState(false); + const [showAllUsers, setShowAllUsers] = useState(false); + const theme = useTheme(); + + const handleDelete = (email: string) => { + setUsersData(usersData.filter(user => user.email !== email)); + if (setDisableSave) { + setDisableSave(false); + } + }; + + const handleAdd = (_: React.ChangeEvent<{}>, value: User | null) => { + if (value) { + setUsersData(prevData => { + prevData = prevData || []; + const isDuplicate = prevData.some(user => user.user_id === value.user_id); + const isDeleted = value.deleted_at?.Valid === true; + + if (isDuplicate || isDeleted) { + setError(isDuplicate ? "User already selected" : "User does not exist"); + return prevData; + } + + setError(false); + return [...prevData, value]; + }); + setInputValue(""); + setOptions([]); + if (setDisableSave) { + setDisableSave(false); + } + } + }; + + const handleInputChange = (_: React.ChangeEvent<{}>, value: string) => { + if (value === "") { + setOptions([]); + setOpen(false); + } else { + setSearchUserLoading(true); + fetchSuggestions(value).then(filteredData => { + setOptions(filteredData); + setSearchUserLoading(false); + }); + setError(false); + setOpen(true); + } + }; + + /** + * Clone customUsersList component to pass necessary props + */ + const clonedComponent = React.cloneElement(customUsersList, { + handleDelete: handleDelete + }); + + const renderChip = (avatarObj: User) => ( + + {avatarObj.avatar_url ? "" : avatarObj.first_name?.charAt(0)} + + } + label={avatarObj.email} + size="small" + onDelete={() => handleDelete(avatarObj.email)} + deleteIcon={ + + + + } + /> + ); + + return ( + <> + x} + options={options} + disableClearable + includeInputInList + filterSelectedOptions + disableListWrap + disabled={disabled} + open={open} + loading={searchUserLoading} + value={inputValue} + getOptionLabel={option => ""} + noOptionsText={searchUserLoading ? "Loading..." : "No users found"} + onChange={handleAdd} + onInputChange={handleInputChange} + isOptionEqualToValue={(option, value) => option === value} + clearOnBlur + renderInput={params => ( + + {searchUserLoading ? ( + + ) : null} + + ) + }} + /> + )} + renderOption={(props, option) => ( + img": { mr: 2, flexShrink: 0 } }} {...props}> + + + + + {option.avatar_url ? "" : } + + + + + {option.deleted_at?.Valid ? ( + + {option.email} (deleted) + + ) : ( + <> + + {option.first_name} {option.last_name} + + + {option.email} + + + )} + + + + )} + /> + + {customUsersList ? ( + clonedComponent + ) : ( + 0 ? "0.5rem" : "" + }} + > + {showAllUsers + ? usersData?.map((avatarObj) => renderChip(avatarObj)) + : usersData?.length > 0 && renderChip(usersData[usersData.length - 1])} + {usersData?.length > 1 && ( + setShowAllUsers(!showAllUsers)} + sx={{ + cursor: "pointer", + color: theme.palette.test.neutral.default, + fontWeight: "600", + "&:hover": { + color: theme.palette.text.brand + } + }} + > + {showAllUsers ? "(hide)" : `(+${usersData.length - 1})`} + + )} + + )} + + ); +}; + +export default UserSearchField; diff --git a/src/custom/UserSearchField/index.tsx b/src/custom/UserSearchField/index.tsx new file mode 100644 index 00000000..3dede99c --- /dev/null +++ b/src/custom/UserSearchField/index.tsx @@ -0,0 +1,3 @@ +import UserSeachField from './UserSeachField'; + +export { UserSeachField }; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 55c7f0f9..714512e0 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -111,3 +111,4 @@ export type { }; export * from './Dialog'; +export * from './UserSearchField' \ No newline at end of file diff --git a/src/icons/Person/PersonIcon.tsx b/src/icons/Person/PersonIcon.tsx new file mode 100644 index 00000000..2d1b5c19 --- /dev/null +++ b/src/icons/Person/PersonIcon.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import { IconProps } from '../types'; + +export const PersonIcon: FC = ({ width, height, fill = "#5f6368", ...props }) => { + return ( + + + + ); +}; + +export default PanToolIcon; diff --git a/src/icons/Person/index.ts b/src/icons/Person/index.ts new file mode 100644 index 00000000..c31dfbfe --- /dev/null +++ b/src/icons/Person/index.ts @@ -0,0 +1 @@ +export { default as PersonIcon } from './PersonIcon'; From 3a4a43c562f1186ed34d4c90dafbbeabd258d9a7 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 17:39:33 +0000 Subject: [PATCH 02/16] Fix lint issue Signed-off-by: Yash Sharma --- src/icons/Person/PersonIcon.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/icons/Person/PersonIcon.tsx b/src/icons/Person/PersonIcon.tsx index 2d1b5c19..ad247997 100644 --- a/src/icons/Person/PersonIcon.tsx +++ b/src/icons/Person/PersonIcon.tsx @@ -8,10 +8,12 @@ export const PersonIcon: FC = ({ width, height, fill = "#5f6368", ... height={height} viewBox="0 -960 960 960" width={width} - fill={fill}> + fill={fill} + {...props} + > ); }; -export default PanToolIcon; +export default PersonIcon; From ccd9f635d39727d82e32be99778cf0c6d759481f Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 20:31:06 +0000 Subject: [PATCH 03/16] feat: added chain icon Signed-off-by: Yash Sharma --- src/icons/Chain/ChainIcon.tsx | 23 +++++++++++++++++++++++ src/icons/Chain/index.tsx | 1 + 2 files changed, 24 insertions(+) create mode 100644 src/icons/Chain/ChainIcon.tsx create mode 100644 src/icons/Chain/index.tsx diff --git a/src/icons/Chain/ChainIcon.tsx b/src/icons/Chain/ChainIcon.tsx new file mode 100644 index 00000000..f44e1ea5 --- /dev/null +++ b/src/icons/Chain/ChainIcon.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; +import { IconProps } from '../types'; + +const ChainIcon = ({ + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, + fill = "#3C494F", + ...props +}: IconProps): JSX.Element => ( + + + +); + +export default ChainIcon; \ No newline at end of file diff --git a/src/icons/Chain/index.tsx b/src/icons/Chain/index.tsx new file mode 100644 index 00000000..043710db --- /dev/null +++ b/src/icons/Chain/index.tsx @@ -0,0 +1 @@ +export { default as ChainIcon } from './ChainIcon'; From 23841cca90898eb5b7e6af8c66a853970a019074 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 20:31:28 +0000 Subject: [PATCH 04/16] feat: fix PersonIcon Signed-off-by: Yash Sharma --- src/icons/Person/PersonIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icons/Person/PersonIcon.tsx b/src/icons/Person/PersonIcon.tsx index ad247997..eb017111 100644 --- a/src/icons/Person/PersonIcon.tsx +++ b/src/icons/Person/PersonIcon.tsx @@ -1,7 +1,7 @@ import { FC } from 'react'; import { IconProps } from '../types'; -export const PersonIcon: FC = ({ width, height, fill = "#5f6368", ...props }) => { +export const PersonIcon: FC = ({ width, height, fill = "#5f6368", ...props }: IconProps) => { return ( Date: Thu, 11 Jul 2024 20:32:01 +0000 Subject: [PATCH 05/16] Fix userSearchField export typo Signed-off-by: Yash Sharma --- src/custom/UserSearchField/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/custom/UserSearchField/index.tsx b/src/custom/UserSearchField/index.tsx index 3dede99c..9d15864a 100644 --- a/src/custom/UserSearchField/index.tsx +++ b/src/custom/UserSearchField/index.tsx @@ -1,3 +1,3 @@ -import UserSeachField from './UserSeachField'; +import UserSearchField from './UserSeachField'; -export { UserSeachField }; +export { UserSearchField }; From d1e6a6870d437144165e259996e7046de614c8cc Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 20:32:22 +0000 Subject: [PATCH 06/16] feat: added share modal Signed-off-by: Yash Sharma --- src/custom/ShareModal/ShareModal.tsx | 273 +++++++++++++++++++++++++++ src/custom/ShareModal/style.tsx | 64 +++++++ src/icons/index.ts | 1 + 3 files changed, 338 insertions(+) create mode 100644 src/custom/ShareModal/ShareModal.tsx create mode 100644 src/custom/ShareModal/style.tsx diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx new file mode 100644 index 00000000..368891b7 --- /dev/null +++ b/src/custom/ShareModal/ShareModal.tsx @@ -0,0 +1,273 @@ +import React, { useEffect, useState } from 'react'; +import { + Avatar, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, + MenuItem, + Typography, +} from '../../base'; +import { useTheme } from "../../theme" +import { Modal, ModalBody, ModalFooter, ModalButtonPrimary, ModalButtonSecondary } from "../Modal"; +import { UserSearchField } from '../UserSearchField'; +import { ListWrapper, CustomListItemText, IconButtonWrapper, VisibilityIconWrapper, FormControlWrapper, CustomSelect, CustomDialogContentText } from './style'; +import { ChainIcon } from "../../icons/Chain"; +import { DeleteIcon } from "../../icons/Delete"; + +const options = { + PUBLIC: 'Anyone with the link can edit', + PRIVATE: 'Only people with access can open with the link', +}; + +const SHARE_MODE = { + PRIVATE: 'private', + PUBLIC: 'public', +}; + +interface User { + id: string; + first_name: string; + last_name: string; + email: string; + avatar_url: string; +} + +interface AccessListProps { + accessList: User[]; + ownerData: User; + handleDelete: (email: string) => void; + hostURL?: string | null; +} + +/** + * Custom component to show users list with delete icon and owner tag +*/ +const AccessList: React.FC = ({ accessList, ownerData, handleDelete, hostURL }: AccessListProps) => { + const openInNewTab = (url: string) => { + window.open(url, '_blank', 'noreferrer'); + }; + + return ( + + + {accessList.map((actorData) => ( + + + { + hostURL && openInNewTab(`${hostURL}/user/${actorData.id}`) + }} + /> + + + + {ownerData.id === actorData.id ? ( +
Owner
+ ) : ( + handleDelete(actorData.email)}> + + + )} + + + ))} + + + ); +}; + +interface ShareModalProps { + /** Function to close the share modal */ + handleShareModalClose: () => void; + /** The resource that is selected for sharing. The type is `any` because it can vary based on the application context */ + selectedResource: any; + /** The name of the data being shared, like design or filter */ + dataName: string; + /** Data of the user who owns the resource */ + ownerData: User; + /** Function to fetch the list of users who have access to the resource */ + fetchAccessActors: () => Promise; + /** Function to handle the sharing of the resource with specified users and options */ + handleShare: (shareUserData: User[], selectedOption: string) => void; + /** Optional URL of the host application. Defaults to `null` if not provided */ + hostURL?: string | null; + /** Optional flag to disable the visibility selector. Defaults to `false` if not provided */ + isVisibilitySelectorDisabled?: boolean; +} + +/** + * ShareModal component allows sharing a resource with specified users + * and configuring visibility options. + */ +const ShareModal: React.FC = ({ + handleShareModalClose, + selectedResource, + dataName, + ownerData, + fetchAccessActors, + handleShare, + hostURL = null, + isVisibilitySelectorDisabled = false +}: ShareModalProps): JSX.Element => { + const theme = useTheme(); + const [openMenu, setMenu] = useState(false); + const [selectedOption, setOption] = useState(selectedResource?.visibility); + const [shareUserData, setShareUserData] = useState([]); + + const handleDelete = (email: string) => { + setShareUserData((prevData) => prevData.filter((user) => user.email !== email)); + }; + + const handleOptionClick = (event: React.ChangeEvent<{ value: unknown }>) => { + const value = event.target.value as string; + setOption(value); + }; + + const handleMenuClose = () => setMenu(false); + + /** + * Copy design link in clipboard + */ + const handleCopy = () => { + navigator.clipboard.writeText(getCatalogUrl(catalogType, cardName, cardId)); + }; + + const isShareDisabled = () => { + const existingAccessIds = shareUserData.map((user) => user.id); + const ownerDataId = ownerData?.id; + + if (ownerDataId) { + existingAccessIds.push(ownerDataId); + } + + const hasMismatchedUsers = !shareUserData.every((user) => + existingAccessIds.includes(user.id) + ); + + return ( + shareUserData.length === existingAccessIds.length && + !hasMismatchedUsers && + (selectedOption === selectedResource?.visibility || + shareUserData.length !== existingAccessIds.length) + ); + }; + + useEffect(() => { + const fetchActors = async () => { + const actors = await fetchAccessActors(); + setShareUserData(actors); + }; + fetchActors(); + }, [fetchAccessActors]); + + useEffect(() => { + if (selectedResource) { + setOption(selectedResource?.visibility); + } + }, [selectedResource]); + + return ( +
+ + + + } + /> + + + + General Access + + + +
+ + {selectedOption === SHARE_MODE.PUBLIC ? ( + + ) : ( + + )} + +
+ setMenu(true)} + onChange={handleOptionClick} + disabled={isVisibilitySelectorDisabled} + > + {Object.values(SHARE_MODE).map((option) => ( + + {option.charAt(0).toUpperCase() + option.slice(1)} + + ))} + + + {selectedOption === SHARE_MODE.PRIVATE ? options.PRIVATE : options.PUBLIC} + +
+
+
+
+
+ + + + + + + Copy Link + + handleShare(shareUserData, selectedOption)} + > + Share + + +
+
+ ); +}; + +export default ShareModal; diff --git a/src/custom/ShareModal/style.tsx b/src/custom/ShareModal/style.tsx new file mode 100644 index 00000000..b38607a3 --- /dev/null +++ b/src/custom/ShareModal/style.tsx @@ -0,0 +1,64 @@ +import { styled } from '@mui/material'; +import { Select, ListItemText, Typography, FormControl, DialogContentText } from "../../base"; + +export const CustomText = styled(Typography)(() => ({ + fontFamily: "Qanelas Soft, sans-serif", + "&.MuiTypography-root": { + fontFamily: "Qanelas Soft, sans-serif" + } +})); + +export const CustomListItemText = styled(ListItemText)(() => ({ + display: "flex", + justifyContent: "space-between" +})); + +export const IconButtonWrapper = styled(`div`)(() => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginRight: '0.2rem', +})); + +export const VisibilityIconWrapper = styled(`div`)(({ theme }) => ({ + width: '36px', + height: '36px', + background: theme.palette.secondary.tabHover, + borderRadius: '20px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginRight: '1rem', +})); + +export const FormControlWrapper = styled(FormControl)(() => ({ + width: "100%" +})); + +export const ListWrapper = styled(`div`)(({ theme }) => ({ + maxHeight: "16rem", + overflowY: "auto", + color: theme.palette.secondary.text +})); + +export const CustomSelect = styled(Select)(() => ({ + width: "6rem", + "&:before": { + display: "none" + }, + "&:after": { + display: "none" + }, + fontFamily: "Qanelas Soft, sans-serif", + "&.MuiTypography-root": { + fontFamily: "Qanelas Soft, sans-serif" + } +})); + +export const CustomDialogContentText = styled(DialogContentText)(() => ({ + display: 'flex', + justifyContent: 'space-between', + marginTop: '0.2rem', + alignItems: 'center', + alignContent: 'center', +})); diff --git a/src/icons/index.ts b/src/icons/index.ts index bafdc2b1..99aaa7a2 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -86,3 +86,4 @@ export * from './Validate'; export * from './Visibility'; export * from './Visualizer'; export * from './Workspace'; +export * from './Chain'; \ No newline at end of file From ff851a7444fe2250f359ef3ea8301ece88065a69 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 21:36:55 +0000 Subject: [PATCH 07/16] feat: fix lint issues Signed-off-by: Yash Sharma --- src/custom/ShareModal/ShareModal.tsx | 473 +++++++++--------- src/custom/ShareModal/style.tsx | 78 +-- .../UserSearchField/UserSearchField.tsx | 426 ++++++++-------- src/custom/index.tsx | 2 +- src/icons/Chain/ChainIcon.tsx | 34 +- src/icons/Person/PersonIcon.tsx | 9 +- src/icons/index.ts | 2 +- 7 files changed, 524 insertions(+), 500 deletions(-) diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 368891b7..823f8008 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -1,107 +1,124 @@ import React, { useEffect, useState } from 'react'; import { - Avatar, - IconButton, - List, - ListItem, - ListItemAvatar, - ListItemSecondaryAction, - ListItemText, - MenuItem, - Typography, + Avatar, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, + MenuItem, + Typography } from '../../base'; -import { useTheme } from "../../theme" -import { Modal, ModalBody, ModalFooter, ModalButtonPrimary, ModalButtonSecondary } from "../Modal"; +import { ChainIcon } from '../../icons/Chain'; +import { DeleteIcon } from '../../icons/Delete'; +import { useTheme } from '../../theme'; +import { Modal, ModalBody, ModalButtonPrimary, ModalButtonSecondary, ModalFooter } from '../Modal'; import { UserSearchField } from '../UserSearchField'; -import { ListWrapper, CustomListItemText, IconButtonWrapper, VisibilityIconWrapper, FormControlWrapper, CustomSelect, CustomDialogContentText } from './style'; -import { ChainIcon } from "../../icons/Chain"; -import { DeleteIcon } from "../../icons/Delete"; +import { + CustomDialogContentText, + CustomListItemText, + CustomSelect, + FormControlWrapper, + IconButtonWrapper, + ListWrapper, + VisibilityIconWrapper +} from './style'; const options = { - PUBLIC: 'Anyone with the link can edit', - PRIVATE: 'Only people with access can open with the link', + PUBLIC: 'Anyone with the link can edit', + PRIVATE: 'Only people with access can open with the link' }; const SHARE_MODE = { - PRIVATE: 'private', - PUBLIC: 'public', + PRIVATE: 'private', + PUBLIC: 'public' }; interface User { - id: string; - first_name: string; - last_name: string; - email: string; - avatar_url: string; + id: string; + first_name: string; + last_name: string; + email: string; + avatar_url: string; } interface AccessListProps { - accessList: User[]; - ownerData: User; - handleDelete: (email: string) => void; - hostURL?: string | null; + accessList: User[]; + ownerData: User; + handleDelete: (email: string) => void; + hostURL?: string | null; } /** * Custom component to show users list with delete icon and owner tag -*/ -const AccessList: React.FC = ({ accessList, ownerData, handleDelete, hostURL }: AccessListProps) => { - const openInNewTab = (url: string) => { - window.open(url, '_blank', 'noreferrer'); - }; + */ +const AccessList: React.FC = ({ + accessList, + ownerData, + handleDelete, + hostURL +}: AccessListProps) => { + const openInNewTab = (url: string) => { + window.open(url, '_blank', 'noreferrer'); + }; - return ( - - - {accessList.map((actorData) => ( - - - { - hostURL && openInNewTab(`${hostURL}/user/${actorData.id}`) - }} - /> - - - - {ownerData.id === actorData.id ? ( -
Owner
- ) : ( - handleDelete(actorData.email)}> - - - )} -
-
- ))} -
-
- ); + return ( + + + {accessList.map((actorData) => ( + + + { + hostURL && openInNewTab(`${hostURL}/user/${actorData.id}`); + }} + /> + + + + {ownerData.id === actorData.id ? ( +
Owner
+ ) : ( + handleDelete(actorData.email)} + > + + + )} +
+
+ ))} +
+
+ ); }; interface ShareModalProps { - /** Function to close the share modal */ - handleShareModalClose: () => void; - /** The resource that is selected for sharing. The type is `any` because it can vary based on the application context */ - selectedResource: any; - /** The name of the data being shared, like design or filter */ - dataName: string; - /** Data of the user who owns the resource */ - ownerData: User; - /** Function to fetch the list of users who have access to the resource */ - fetchAccessActors: () => Promise; - /** Function to handle the sharing of the resource with specified users and options */ - handleShare: (shareUserData: User[], selectedOption: string) => void; - /** Optional URL of the host application. Defaults to `null` if not provided */ - hostURL?: string | null; - /** Optional flag to disable the visibility selector. Defaults to `false` if not provided */ - isVisibilitySelectorDisabled?: boolean; + /** Function to close the share modal */ + handleShareModalClose: () => void; + /** The resource that is selected for sharing. The type is `object` because it can vary based on the application context */ + selectedResource: object; + /** The name of the data being shared, like design or filter */ + dataName: string; + /** Data of the user who owns the resource */ + ownerData: User; + /** Function to fetch the list of users who have access to the resource */ + fetchAccessActors: () => Promise; + /** Function to handle the sharing of the resource with specified users and options */ + handleShare: (shareUserData: User[], selectedOption: string) => void; + /** Optional URL of the host application. Defaults to `null` if not provided */ + hostURL?: string | null; + /** Optional flag to disable the visibility selector. Defaults to `false` if not provided */ + isVisibilitySelectorDisabled?: boolean; } /** @@ -109,165 +126,169 @@ interface ShareModalProps { * and configuring visibility options. */ const ShareModal: React.FC = ({ - handleShareModalClose, - selectedResource, - dataName, - ownerData, - fetchAccessActors, - handleShare, - hostURL = null, - isVisibilitySelectorDisabled = false + handleShareModalClose, + selectedResource, + dataName, + ownerData, + fetchAccessActors, + handleShare, + hostURL = null, + isVisibilitySelectorDisabled = false }: ShareModalProps): JSX.Element => { - const theme = useTheme(); - const [openMenu, setMenu] = useState(false); - const [selectedOption, setOption] = useState(selectedResource?.visibility); - const [shareUserData, setShareUserData] = useState([]); + const theme = useTheme(); + const [openMenu, setMenu] = useState(false); + const [selectedOption, setOption] = useState(selectedResource?.visibility); + const [shareUserData, setShareUserData] = useState([]); - const handleDelete = (email: string) => { - setShareUserData((prevData) => prevData.filter((user) => user.email !== email)); - }; + const handleDelete = (email: string) => { + setShareUserData((prevData) => prevData.filter((user) => user.email !== email)); + }; - const handleOptionClick = (event: React.ChangeEvent<{ value: unknown }>) => { - const value = event.target.value as string; - setOption(value); - }; + const handleOptionClick = (event: React.ChangeEvent<{ value: unknown }>) => { + const value = event.target.value as string; + setOption(value); + }; - const handleMenuClose = () => setMenu(false); + const handleMenuClose = () => setMenu(false); - /** - * Copy design link in clipboard - */ - const handleCopy = () => { - navigator.clipboard.writeText(getCatalogUrl(catalogType, cardName, cardId)); - }; + /** + * Copy design link in clipboard + */ + const handleCopy = () => { + navigator.clipboard.writeText(getCatalogUrl(catalogType, cardName, cardId)); + }; - const isShareDisabled = () => { - const existingAccessIds = shareUserData.map((user) => user.id); - const ownerDataId = ownerData?.id; + const isShareDisabled = () => { + const existingAccessIds = shareUserData.map((user) => user.id); + const ownerDataId = ownerData?.id; - if (ownerDataId) { - existingAccessIds.push(ownerDataId); - } + if (ownerDataId) { + existingAccessIds.push(ownerDataId); + } - const hasMismatchedUsers = !shareUserData.every((user) => - existingAccessIds.includes(user.id) - ); + const hasMismatchedUsers = !shareUserData.every((user) => existingAccessIds.includes(user.id)); - return ( - shareUserData.length === existingAccessIds.length && - !hasMismatchedUsers && - (selectedOption === selectedResource?.visibility || - shareUserData.length !== existingAccessIds.length) - ); - }; + return ( + shareUserData.length === existingAccessIds.length && + !hasMismatchedUsers && + (selectedOption === selectedResource?.visibility || + shareUserData.length !== existingAccessIds.length) + ); + }; - useEffect(() => { - const fetchActors = async () => { - const actors = await fetchAccessActors(); - setShareUserData(actors); - }; - fetchActors(); - }, [fetchAccessActors]); + useEffect(() => { + const fetchActors = async () => { + const actors = await fetchAccessActors(); + setShareUserData(actors); + }; + fetchActors(); + }, [fetchAccessActors]); - useEffect(() => { - if (selectedResource) { - setOption(selectedResource?.visibility); - } - }, [selectedResource]); + useEffect(() => { + if (selectedResource) { + setOption(selectedResource?.visibility); + } + }, [selectedResource]); - return ( -
- - - - } + return ( +
+ + + + } + /> + + + General Access + + +
+ + {selectedOption === SHARE_MODE.PUBLIC ? ( + + ) : ( + - - - - General Access - - - -
- - {selectedOption === SHARE_MODE.PUBLIC ? ( - - ) : ( - - )} - -
- setMenu(true)} - onChange={handleOptionClick} - disabled={isVisibilitySelectorDisabled} - > - {Object.values(SHARE_MODE).map((option) => ( - - {option.charAt(0).toUpperCase() + option.slice(1)} - - ))} - - - {selectedOption === SHARE_MODE.PRIVATE ? options.PRIVATE : options.PUBLIC} - -
-
-
-
-
+ )} +
+
+ setMenu(true)} + onChange={handleOptionClick} + disabled={isVisibilitySelectorDisabled} + > + {Object.values(SHARE_MODE).map((option) => ( + + {option.charAt(0).toUpperCase() + option.slice(1)} + + ))} + + + {selectedOption === SHARE_MODE.PRIVATE ? options.PRIVATE : options.PUBLIC} + +
+
+
+
+
- - - - - - Copy Link - - handleShare(shareUserData, selectedOption)} - > - Share - - -
-
- ); + + + + + + Copy Link + + handleShare(shareUserData, selectedOption)} + > + Share + + +
+
+ ); }; export default ShareModal; diff --git a/src/custom/ShareModal/style.tsx b/src/custom/ShareModal/style.tsx index b38607a3..6d4ce160 100644 --- a/src/custom/ShareModal/style.tsx +++ b/src/custom/ShareModal/style.tsx @@ -1,64 +1,64 @@ import { styled } from '@mui/material'; -import { Select, ListItemText, Typography, FormControl, DialogContentText } from "../../base"; +import { DialogContentText, FormControl, ListItemText, Select, Typography } from '../../base'; export const CustomText = styled(Typography)(() => ({ - fontFamily: "Qanelas Soft, sans-serif", - "&.MuiTypography-root": { - fontFamily: "Qanelas Soft, sans-serif" - } + fontFamily: 'Qanelas Soft, sans-serif', + '&.MuiTypography-root': { + fontFamily: 'Qanelas Soft, sans-serif' + } })); export const CustomListItemText = styled(ListItemText)(() => ({ - display: "flex", - justifyContent: "space-between" + display: 'flex', + justifyContent: 'space-between' })); export const IconButtonWrapper = styled(`div`)(() => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginRight: '0.2rem', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginRight: '0.2rem' })); export const VisibilityIconWrapper = styled(`div`)(({ theme }) => ({ - width: '36px', - height: '36px', - background: theme.palette.secondary.tabHover, - borderRadius: '20px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginRight: '1rem', + width: '36px', + height: '36px', + background: theme.palette.secondary.tabHover, + borderRadius: '20px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginRight: '1rem' })); export const FormControlWrapper = styled(FormControl)(() => ({ - width: "100%" + width: '100%' })); export const ListWrapper = styled(`div`)(({ theme }) => ({ - maxHeight: "16rem", - overflowY: "auto", - color: theme.palette.secondary.text + maxHeight: '16rem', + overflowY: 'auto', + color: theme.palette.secondary.text })); export const CustomSelect = styled(Select)(() => ({ - width: "6rem", - "&:before": { - display: "none" - }, - "&:after": { - display: "none" - }, - fontFamily: "Qanelas Soft, sans-serif", - "&.MuiTypography-root": { - fontFamily: "Qanelas Soft, sans-serif" - } + width: '6rem', + '&:before': { + display: 'none' + }, + '&:after': { + display: 'none' + }, + fontFamily: 'Qanelas Soft, sans-serif', + '&.MuiTypography-root': { + fontFamily: 'Qanelas Soft, sans-serif' + } })); export const CustomDialogContentText = styled(DialogContentText)(() => ({ - display: 'flex', - justifyContent: 'space-between', - marginTop: '0.2rem', - alignItems: 'center', - alignContent: 'center', + display: 'flex', + justifyContent: 'space-between', + marginTop: '0.2rem', + alignItems: 'center', + alignContent: 'center' })); diff --git a/src/custom/UserSearchField/UserSearchField.tsx b/src/custom/UserSearchField/UserSearchField.tsx index e782a094..d0b5a4cd 100644 --- a/src/custom/UserSearchField/UserSearchField.tsx +++ b/src/custom/UserSearchField/UserSearchField.tsx @@ -1,237 +1,233 @@ -import React, { useState } from "react"; -import { PersonIcon } from "../../icons/Person"; +import Autocomplete from '@mui/material/Autocomplete'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useState } from 'react'; +import { Avatar, Box, Chip, Grid, TextField, Tooltip, Typography } from '../../base'; import { iconSmall } from '../../constants/iconsSizes'; -import { CloseIcon } from "../../icons/Close"; -import { Box, Chip, Grid, TextField, Tooltip, Typography, Avatar } from "../../base" -import Autocomplete from "@mui/material/Autocomplete"; -import CircularProgress from "@mui/material/CircularProgress"; -import { useTheme } from "../../theme" +import { CloseIcon } from '../../icons/Close'; +import { PersonIcon } from '../../icons/Person'; +import { useTheme } from '../../theme'; interface User { - user_id: string; - first_name: string; - last_name: string; - email: string; - avatar_url?: string; - deleted_at?: { Valid: boolean }; + user_id: string; + first_name: string; + last_name: string; + email: string; + avatar_url?: string; + deleted_at?: { Valid: boolean }; } interface UserSearchFieldProps { - // Array of user objects currently selected. - usersData: User[]; - // Function to update the selected users data. - setUsersData: (users: User[]) => void; - // Label for the text field. - label?: string; - // Function to enable or disable the save button. - setDisableSave?: (disabled: boolean) => void; - // Type of search being performed, e.g., 'user', 'admin'. - searchType?: string; - // Boolean indicating whether the search field is disabled. - disabled?: boolean; - // Custom component to change rendering style of users list, if not given - // by default it will show list with avatar and email of selected users - customUsersList?: JSX.Element; - /** - * Function to fetch user suggestions based on the input value. - * @param {string} value - The input value for which suggestions are to be fetched. - * @returns {Promise} A promise that resolves to an array of user suggestions. - */ - fetchSuggestions: (value: string) => Promise; + // Array of user objects currently selected. + usersData: User[]; + // Function to update the selected users data. + setUsersData: (users: User[]) => void; + // Label for the text field. + label?: string; + // Function to enable or disable the save button. + setDisableSave?: (disabled: boolean) => void; + // Type of search being performed, e.g., 'user', 'admin'. + searchType?: string; + // Boolean indicating whether the search field is disabled. + disabled?: boolean; + // Custom component to change rendering style of users list, if not given + // by default it will show list with avatar and email of selected users + customUsersList?: JSX.Element; + /** + * Function to fetch user suggestions based on the input value. + * @param {string} value - The input value for which suggestions are to be fetched. + * @returns {Promise} A promise that resolves to an array of user suggestions. + */ + fetchSuggestions: (value: string) => Promise; } const UserSearchField: React.FC = ({ - usersData, - setUsersData, - label, - setDisableSave, - disabled = false, - customUsersList, - fetchSuggestions + usersData, + setUsersData, + label, + setDisableSave, + disabled = false, + customUsersList, + fetchSuggestions }: UserSearchFieldProps) => { - const [error, setError] = useState(false); - const [inputValue, setInputValue] = useState(""); - const [options, setOptions] = useState([]); - const [open, setOpen] = useState(false); - const [searchUserLoading, setSearchUserLoading] = useState(false); - const [showAllUsers, setShowAllUsers] = useState(false); - const theme = useTheme(); + const [error, setError] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [options, setOptions] = useState([]); + const [open, setOpen] = useState(false); + const [searchUserLoading, setSearchUserLoading] = useState(false); + const [showAllUsers, setShowAllUsers] = useState(false); + const theme = useTheme(); - const handleDelete = (email: string) => { - setUsersData(usersData.filter(user => user.email !== email)); - if (setDisableSave) { - setDisableSave(false); - } - }; - - const handleAdd = (_: React.ChangeEvent<{}>, value: User | null) => { - if (value) { - setUsersData(prevData => { - prevData = prevData || []; - const isDuplicate = prevData.some(user => user.user_id === value.user_id); - const isDeleted = value.deleted_at?.Valid === true; + const handleDelete = (email: string) => { + setUsersData(usersData.filter((user) => user.email !== email)); + if (setDisableSave) { + setDisableSave(false); + } + }; - if (isDuplicate || isDeleted) { - setError(isDuplicate ? "User already selected" : "User does not exist"); - return prevData; - } + const handleAdd = (_: React.ChangeEvent, value: User | null) => { + if (value) { + setUsersData((prevData) => { + prevData = prevData || []; + const isDuplicate = prevData.some((user) => user.user_id === value.user_id); + const isDeleted = value.deleted_at?.Valid === true; - setError(false); - return [...prevData, value]; - }); - setInputValue(""); - setOptions([]); - if (setDisableSave) { - setDisableSave(false); - } + if (isDuplicate || isDeleted) { + setError(isDuplicate ? 'User already selected' : 'User does not exist'); + return prevData; } - }; - const handleInputChange = (_: React.ChangeEvent<{}>, value: string) => { - if (value === "") { - setOptions([]); - setOpen(false); - } else { - setSearchUserLoading(true); - fetchSuggestions(value).then(filteredData => { - setOptions(filteredData); - setSearchUserLoading(false); - }); - setError(false); - setOpen(true); - } - }; + setError(false); + return [...prevData, value]; + }); + setInputValue(''); + setOptions([]); + if (setDisableSave) { + setDisableSave(false); + } + } + }; - /** - * Clone customUsersList component to pass necessary props - */ - const clonedComponent = React.cloneElement(customUsersList, { - handleDelete: handleDelete - }); + const handleInputChange = (_: React.ChangeEvent, value: string) => { + if (value === '') { + setOptions([]); + setOpen(false); + } else { + setSearchUserLoading(true); + fetchSuggestions(value).then((filteredData) => { + setOptions(filteredData); + setSearchUserLoading(false); + }); + setError(false); + setOpen(true); + } + }; - const renderChip = (avatarObj: User) => ( - - {avatarObj.avatar_url ? "" : avatarObj.first_name?.charAt(0)} - - } - label={avatarObj.email} - size="small" - onDelete={() => handleDelete(avatarObj.email)} - deleteIcon={ - - - - } - /> - ); + /** + * Clone customUsersList component to pass necessary props + */ + const clonedComponent = React.cloneElement(customUsersList, { + handleDelete: handleDelete + }); - return ( - <> - x} - options={options} - disableClearable - includeInputInList - filterSelectedOptions - disableListWrap - disabled={disabled} - open={open} - loading={searchUserLoading} - value={inputValue} - getOptionLabel={option => ""} - noOptionsText={searchUserLoading ? "Loading..." : "No users found"} - onChange={handleAdd} - onInputChange={handleInputChange} - isOptionEqualToValue={(option, value) => option === value} - clearOnBlur - renderInput={params => ( - - {searchUserLoading ? ( - - ) : null} - - ) - }} - /> - )} - renderOption={(props, option) => ( - img": { mr: 2, flexShrink: 0 } }} {...props}> - - - - - {option.avatar_url ? "" : } - - - - - {option.deleted_at?.Valid ? ( - - {option.email} (deleted) - - ) : ( - <> - - {option.first_name} {option.last_name} - - - {option.email} - - - )} - - - - )} - /> - - {customUsersList ? ( - clonedComponent - ) : ( - 0 ? "0.5rem" : "" - }} - > - {showAllUsers - ? usersData?.map((avatarObj) => renderChip(avatarObj)) - : usersData?.length > 0 && renderChip(usersData[usersData.length - 1])} - {usersData?.length > 1 && ( - setShowAllUsers(!showAllUsers)} - sx={{ - cursor: "pointer", - color: theme.palette.test.neutral.default, - fontWeight: "600", - "&:hover": { - color: theme.palette.text.brand - } - }} - > - {showAllUsers ? "(hide)" : `(+${usersData.length - 1})`} - - )} + const renderChip = (avatarObj: User) => ( + + {avatarObj.avatar_url ? '' : avatarObj.first_name?.charAt(0)} + + } + label={avatarObj.email} + size="small" + onDelete={() => handleDelete(avatarObj.email)} + deleteIcon={ + + + + } + /> + ); + + return ( + <> + x} + options={options} + disableClearable + includeInputInList + filterSelectedOptions + disableListWrap + disabled={disabled} + open={open} + loading={searchUserLoading} + value={inputValue} + getOptionLabel={() => ''} + noOptionsText={searchUserLoading ? 'Loading...' : 'No users found'} + onChange={handleAdd} + onInputChange={handleInputChange} + isOptionEqualToValue={(option, value) => option === value} + clearOnBlur + renderInput={(params) => ( + {searchUserLoading ? : null} + ) + }} + /> + )} + renderOption={(props, option) => ( + img': { mr: 2, flexShrink: 0 } }} {...props}> + + + + + {option.avatar_url ? '' : } + - )} - - ); + + + {option.deleted_at?.Valid ? ( + + {option.email} (deleted) + + ) : ( + <> + + {option.first_name} {option.last_name} + + + {option.email} + + + )} + + + + )} + /> + + {customUsersList ? ( + clonedComponent + ) : ( + 0 ? '0.5rem' : '' + }} + > + {showAllUsers + ? usersData?.map((avatarObj) => renderChip(avatarObj)) + : usersData?.length > 0 && renderChip(usersData[usersData.length - 1])} + {usersData?.length > 1 && ( + setShowAllUsers(!showAllUsers)} + sx={{ + cursor: 'pointer', + color: theme.palette.test.neutral.default, + fontWeight: '600', + '&:hover': { + color: theme.palette.text.brand + } + }} + > + {showAllUsers ? '(hide)' : `(+${usersData.length - 1})`} + + )} + + )} + + ); }; export default UserSearchField; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 714512e0..58a6689e 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -111,4 +111,4 @@ export type { }; export * from './Dialog'; -export * from './UserSearchField' \ No newline at end of file +export * from './UserSearchField'; diff --git a/src/icons/Chain/ChainIcon.tsx b/src/icons/Chain/ChainIcon.tsx index f44e1ea5..bd1842f5 100644 --- a/src/icons/Chain/ChainIcon.tsx +++ b/src/icons/Chain/ChainIcon.tsx @@ -1,23 +1,25 @@ -import React from "react"; import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; import { IconProps } from '../types'; const ChainIcon = ({ - width = DEFAULT_WIDTH, - height = DEFAULT_HEIGHT, - fill = "#3C494F", - ...props + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, + fill = '#3C494F', + ...props }: IconProps): JSX.Element => ( - - - + + + ); -export default ChainIcon; \ No newline at end of file +export default ChainIcon; diff --git a/src/icons/Person/PersonIcon.tsx b/src/icons/Person/PersonIcon.tsx index eb017111..3d6dec76 100644 --- a/src/icons/Person/PersonIcon.tsx +++ b/src/icons/Person/PersonIcon.tsx @@ -1,7 +1,12 @@ import { FC } from 'react'; import { IconProps } from '../types'; -export const PersonIcon: FC = ({ width, height, fill = "#5f6368", ...props }: IconProps) => { +export const PersonIcon: FC = ({ + width, + height, + fill = '#5f6368', + ...props +}: IconProps) => { return ( = ({ width, height, fill = "#5f6368", ... width={width} fill={fill} {...props} - > + > ); diff --git a/src/icons/index.ts b/src/icons/index.ts index 99aaa7a2..37338075 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -38,6 +38,7 @@ export * from './Mesh'; // export { default as ModifiedApplicationFileIcon } from "./ModifiedApplicationFileIcon"; // export { default as OriginalApplicationFileIcon } from "./OriginalApplicationFileIcon"; export * from './Calender'; +export * from './Chain'; export * from './ChevronLeft'; export * from './ContentClassIcons'; export * from './Deployments'; @@ -86,4 +87,3 @@ export * from './Validate'; export * from './Visibility'; export * from './Visualizer'; export * from './Workspace'; -export * from './Chain'; \ No newline at end of file From f835a8abaa6e99f5217b6d0feda065d47dd5cccb Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 21:55:15 +0000 Subject: [PATCH 08/16] fix: fix typo Signed-off-by: Yash Sharma --- src/custom/UserSearchField/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/custom/UserSearchField/index.tsx b/src/custom/UserSearchField/index.tsx index 9d15864a..c0234808 100644 --- a/src/custom/UserSearchField/index.tsx +++ b/src/custom/UserSearchField/index.tsx @@ -1,3 +1,3 @@ -import UserSearchField from './UserSeachField'; +import UserSearchField from './UserSearchField'; export { UserSearchField }; From 41446e16259c3bbcd8d2dfbd138243821a35cf0d Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 22:01:54 +0000 Subject: [PATCH 09/16] fix: fix user interface type issues Signed-off-by: Yash Sharma --- src/custom/ShareModal/ShareModal.tsx | 14 ++++++++++++-- src/custom/UserSearchField/UserSearchField.tsx | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 823f8008..93968678 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -37,10 +37,12 @@ const SHARE_MODE = { interface User { id: string; + user_id: string; first_name: string; last_name: string; email: string; - avatar_url: string; + avatar_url?: string; + deleted_at?: { Valid: boolean }; } interface AccessListProps { @@ -119,6 +121,12 @@ interface ShareModalProps { hostURL?: string | null; /** Optional flag to disable the visibility selector. Defaults to `false` if not provided */ isVisibilitySelectorDisabled?: boolean; + /** + * Function to fetch user suggestions based on the input value. + * @param {string} value - The input value for which suggestions are to be fetched. + * @returns {Promise} A promise that resolves to an array of user suggestions. + */ + fetchSuggestions: (value: string) => Promise; } /** @@ -133,7 +141,8 @@ const ShareModal: React.FC = ({ fetchAccessActors, handleShare, hostURL = null, - isVisibilitySelectorDisabled = false + isVisibilitySelectorDisabled = false, + fetchSuggestions }: ShareModalProps): JSX.Element => { const theme = useTheme(); const [openMenu, setMenu] = useState(false); @@ -210,6 +219,7 @@ const ShareModal: React.FC = ({ hostURL={hostURL} /> } + fetchSuggestions={fetchSuggestions} /> diff --git a/src/custom/UserSearchField/UserSearchField.tsx b/src/custom/UserSearchField/UserSearchField.tsx index d0b5a4cd..3c7f9479 100644 --- a/src/custom/UserSearchField/UserSearchField.tsx +++ b/src/custom/UserSearchField/UserSearchField.tsx @@ -8,6 +8,7 @@ import { PersonIcon } from '../../icons/Person'; import { useTheme } from '../../theme'; interface User { + id: string; user_id: string; first_name: string; last_name: string; From 5b9dbad755b6f87503ab71476c4fb34cd59cf391 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 22:12:43 +0000 Subject: [PATCH 10/16] feat: add lockIcon and PublicIcon Signed-off-by: Yash Sharma --- src/custom/ShareModal/ShareModal.tsx | 11 +++++------ src/icons/Lock/LockIcon.tsx | 22 ++++++++++++++++++++++ src/icons/Lock/index.tsx | 1 + src/icons/Public/PublicIcon.tsx | 22 ++++++++++++++++++++++ src/icons/Public/index.tsx | 1 + src/icons/index.ts | 2 ++ 6 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/icons/Lock/LockIcon.tsx create mode 100644 src/icons/Lock/index.tsx create mode 100644 src/icons/Public/PublicIcon.tsx create mode 100644 src/icons/Public/index.tsx diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 93968678..2658cdb2 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -10,8 +10,7 @@ import { MenuItem, Typography } from '../../base'; -import { ChainIcon } from '../../icons/Chain'; -import { DeleteIcon } from '../../icons/Delete'; +import { ChainIcon, DeleteIcon, LockIcon, PublicIcon } from '../../icons'; import { useTheme } from '../../theme'; import { Modal, ModalBody, ModalButtonPrimary, ModalButtonSecondary, ModalFooter } from '../Modal'; import { UserSearchField } from '../UserSearchField'; @@ -237,16 +236,16 @@ const ShareModal: React.FC = ({
{selectedOption === SHARE_MODE.PUBLIC ? ( - ) : ( - )} diff --git a/src/icons/Lock/LockIcon.tsx b/src/icons/Lock/LockIcon.tsx new file mode 100644 index 00000000..27759981 --- /dev/null +++ b/src/icons/Lock/LockIcon.tsx @@ -0,0 +1,22 @@ +import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; +import { IconProps } from '../types'; + +const LockIcon = ({ + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, + fill = '#3C494F', + ...props +}: IconProps): JSX.Element => ( + + + +); + +export default LockIcon; diff --git a/src/icons/Lock/index.tsx b/src/icons/Lock/index.tsx new file mode 100644 index 00000000..a46fa12c --- /dev/null +++ b/src/icons/Lock/index.tsx @@ -0,0 +1 @@ +export { default as LockIcon } from './LockIcon'; diff --git a/src/icons/Public/PublicIcon.tsx b/src/icons/Public/PublicIcon.tsx new file mode 100644 index 00000000..65bcd83f --- /dev/null +++ b/src/icons/Public/PublicIcon.tsx @@ -0,0 +1,22 @@ +import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; +import { IconProps } from '../types'; + +const PublicIcon = ({ + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, + fill = '#3C494F', + ...props +}: IconProps): JSX.Element => ( + + + +); + +export default PublicIcon; diff --git a/src/icons/Public/index.tsx b/src/icons/Public/index.tsx new file mode 100644 index 00000000..32de4ff7 --- /dev/null +++ b/src/icons/Public/index.tsx @@ -0,0 +1 @@ +export { default as PublicIcon } from './PublicIcon'; diff --git a/src/icons/index.ts b/src/icons/index.ts index 37338075..931a653c 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -55,11 +55,13 @@ export * from './InfoOutlined'; export * from './Kubernetes'; export * from './LeftAngledArrow'; export * from './LeftArrow'; +export * from './Lock'; export * from './MesheryOperator'; export * from './Open'; export * from './PanTool'; export * from './Pattern'; export * from './Pod'; +export * from './Public'; export * from './Publish'; export * from './Question'; export * from './Read'; From e1ed8b2a8d80d43ad00552e3f4445784496637f0 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 22:35:34 +0000 Subject: [PATCH 11/16] feat: Added people with access Signed-off-by: Yash Sharma --- src/custom/ShareModal/ShareModal.tsx | 75 +++++++++++++++------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 2658cdb2..1052c2cd 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -65,41 +65,48 @@ const AccessList: React.FC = ({ }; return ( - - - {accessList.map((actorData) => ( - - - { - hostURL && openInNewTab(`${hostURL}/user/${actorData.id}`); - }} + <> + {accessList.length > 0 && ( + + People with Access + + )} + + + {accessList.map((actorData) => ( + + + { + hostURL && openInNewTab(`${hostURL}/user/${actorData.id}`); + }} + /> + + - - - - {ownerData.id === actorData.id ? ( -
Owner
- ) : ( - handleDelete(actorData.email)} - > - - - )} -
-
- ))} -
-
+ + {ownerData.id === actorData.id ? ( +
Owner
+ ) : ( + handleDelete(actorData.email)} + > + + + )} +
+ + ))} + + + ); }; From cdf86c26b2157528f89dfd73999c3d8a09aedc82 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 11 Jul 2024 23:47:17 +0000 Subject: [PATCH 12/16] fix: fix lint issues Signed-off-by: Yash Sharma --- .../UserSearchField/UserSearchField.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/custom/UserSearchField/UserSearchField.tsx b/src/custom/UserSearchField/UserSearchField.tsx index 3c7f9479..1023f55e 100644 --- a/src/custom/UserSearchField/UserSearchField.tsx +++ b/src/custom/UserSearchField/UserSearchField.tsx @@ -21,7 +21,7 @@ interface UserSearchFieldProps { // Array of user objects currently selected. usersData: User[]; // Function to update the selected users data. - setUsersData: (users: User[]) => void; + setUsersData: React.Dispatch>; // Label for the text field. label?: string; // Function to enable or disable the save button. @@ -51,7 +51,7 @@ const UserSearchField: React.FC = ({ fetchSuggestions }: UserSearchFieldProps) => { const [error, setError] = useState(false); - const [inputValue, setInputValue] = useState(''); + const [inputValue, setInputValue] = useState(undefined); const [options, setOptions] = useState([]); const [open, setOpen] = useState(false); const [searchUserLoading, setSearchUserLoading] = useState(false); @@ -65,9 +65,9 @@ const UserSearchField: React.FC = ({ } }; - const handleAdd = (_: React.ChangeEvent, value: User | null) => { + const handleAdd = (_event: React.SyntheticEvent, value: User | null) => { if (value) { - setUsersData((prevData) => { + setUsersData((prevData: User[]): User[] => { prevData = prevData || []; const isDuplicate = prevData.some((user) => user.user_id === value.user_id); const isDeleted = value.deleted_at?.Valid === true; @@ -80,7 +80,7 @@ const UserSearchField: React.FC = ({ setError(false); return [...prevData, value]; }); - setInputValue(''); + setInputValue(undefined); // Clear the input value setOptions([]); if (setDisableSave) { setDisableSave(false); @@ -88,7 +88,7 @@ const UserSearchField: React.FC = ({ } }; - const handleInputChange = (_: React.ChangeEvent, value: string) => { + const handleInputChange = (_event: React.SyntheticEvent, value: string) => { if (value === '') { setOptions([]); setOpen(false); @@ -106,9 +106,11 @@ const UserSearchField: React.FC = ({ /** * Clone customUsersList component to pass necessary props */ - const clonedComponent = React.cloneElement(customUsersList, { - handleDelete: handleDelete - }); + const clonedComponent = customUsersList + ? React.cloneElement(customUsersList, { + handleDelete: handleDelete + }) + : null; const renderChip = (avatarObj: User) => ( = ({ }} /> )} - renderOption={(props, option) => ( + renderOption={(props: React.HTMLAttributes, option) => ( + // @ts-expect-error Props need to be passed to BOX component to make sure styles getting updated img': { mr: 2, flexShrink: 0 } }} {...props}> @@ -215,7 +218,7 @@ const UserSearchField: React.FC = ({ onClick={() => setShowAllUsers(!showAllUsers)} sx={{ cursor: 'pointer', - color: theme.palette.test.neutral.default, + color: theme.palette.text.default, fontWeight: '600', '&:hover': { color: theme.palette.text.brand From 310a65e2967cbae6060af017fb989e3055eceaf1 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Fri, 12 Jul 2024 00:04:10 +0000 Subject: [PATCH 13/16] fix: fix share modal export Signed-off-by: Yash Sharma --- src/custom/ShareModal/index.tsx | 3 +++ src/custom/index.tsx | 1 + 2 files changed, 4 insertions(+) create mode 100644 src/custom/ShareModal/index.tsx diff --git a/src/custom/ShareModal/index.tsx b/src/custom/ShareModal/index.tsx new file mode 100644 index 00000000..47ba3828 --- /dev/null +++ b/src/custom/ShareModal/index.tsx @@ -0,0 +1,3 @@ +import ShareModal from './ShareModal'; + +export { ShareModal }; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 58a6689e..823c0302 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -111,4 +111,5 @@ export type { }; export * from './Dialog'; +export * from './ShareModal'; export * from './UserSearchField'; From b19a41142b5fa5c5ad8c5b12be118ece41f051f5 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Fri, 12 Jul 2024 00:34:33 +0000 Subject: [PATCH 14/16] fix: fix share modal export and lint errors Signed-off-by: Yash Sharma --- src/custom/Modal/index.tsx | 4 +-- src/custom/ShareModal/ShareModal.tsx | 40 +++++++++++++++++----------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/custom/Modal/index.tsx b/src/custom/Modal/index.tsx index 6ea90ae6..416049ae 100644 --- a/src/custom/Modal/index.tsx +++ b/src/custom/Modal/index.tsx @@ -9,8 +9,8 @@ import { CustomTooltip } from '../CustomTooltip'; interface ModalProps extends DialogProps { closeModal: () => void; title: string; - headerIcon: React.ReactNode; - reactNode: React.ReactNode; + headerIcon?: React.ReactNode; + reactNode?: React.ReactNode; } interface ModalFooterProps { diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 1052c2cd..1c711f86 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -1,3 +1,4 @@ +import { SelectChangeEvent } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { Avatar, @@ -110,11 +111,17 @@ const AccessList: React.FC = ({ ); }; +interface SelectedResource { + visibility: string; + name: string; + [key: string]: unknown; +} + interface ShareModalProps { /** Function to close the share modal */ handleShareModalClose: () => void; - /** The resource that is selected for sharing. The type is `object` because it can vary based on the application context */ - selectedResource: object; + /** The resource that is selected for sharing.*/ + selectedResource: SelectedResource; /** The name of the data being shared, like design or filter */ dataName: string; /** Data of the user who owns the resource */ @@ -122,9 +129,14 @@ interface ShareModalProps { /** Function to fetch the list of users who have access to the resource */ fetchAccessActors: () => Promise; /** Function to handle the sharing of the resource with specified users and options */ - handleShare: (shareUserData: User[], selectedOption: string) => void; + handleShare: (shareUserData: User[], selectedOption: string | undefined) => void; /** Optional URL of the host application. Defaults to `null` if not provided */ hostURL?: string | null; + /** + * Optional URL of the resource. Defaults to empty string if not provided + * Resource URL will be the URL which user will copy with Copy Link Button + */ + resourceURL?: string; /** Optional flag to disable the visibility selector. Defaults to `false` if not provided */ isVisibilitySelectorDisabled?: boolean; /** @@ -147,6 +159,7 @@ const ShareModal: React.FC = ({ fetchAccessActors, handleShare, hostURL = null, + resourceURL = '', isVisibilitySelectorDisabled = false, fetchSuggestions }: ShareModalProps): JSX.Element => { @@ -159,7 +172,7 @@ const ShareModal: React.FC = ({ setShareUserData((prevData) => prevData.filter((user) => user.email !== email)); }; - const handleOptionClick = (event: React.ChangeEvent<{ value: unknown }>) => { + const handleOptionClick = (event: SelectChangeEvent) => { const value = event.target.value as string; setOption(value); }; @@ -170,7 +183,7 @@ const ShareModal: React.FC = ({ * Copy design link in clipboard */ const handleCopy = () => { - navigator.clipboard.writeText(getCatalogUrl(catalogType, cardName, cardId)); + navigator.clipboard.writeText(resourceURL); }; const isShareDisabled = () => { @@ -229,16 +242,10 @@ const ShareModal: React.FC = ({ /> - General Access - + + General Access + +
@@ -258,6 +265,7 @@ const ShareModal: React.FC = ({
= ({ Copy Link From f77d836fc621db31d170d20b93b71d4b9b7d4c45 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Fri, 12 Jul 2024 10:58:20 +0000 Subject: [PATCH 15/16] fix: fix styles Signed-off-by: Yash Sharma --- src/custom/ShareModal/ShareModal.tsx | 25 ++++++++++--------- src/custom/ShareModal/style.tsx | 14 ++++++++--- .../UserSearchField/UserSearchField.tsx | 4 +++ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/custom/ShareModal/ShareModal.tsx b/src/custom/ShareModal/ShareModal.tsx index 1c711f86..fe7cc747 100644 --- a/src/custom/ShareModal/ShareModal.tsx +++ b/src/custom/ShareModal/ShareModal.tsx @@ -13,6 +13,7 @@ import { } from '../../base'; import { ChainIcon, DeleteIcon, LockIcon, PublicIcon } from '../../icons'; import { useTheme } from '../../theme'; +import { BLACK, WHITE } from '../../theme/colors'; import { Modal, ModalBody, ModalButtonPrimary, ModalButtonSecondary, ModalFooter } from '../Modal'; import { UserSearchField } from '../UserSearchField'; import { @@ -65,6 +66,8 @@ const AccessList: React.FC = ({ window.open(url, '_blank', 'noreferrer'); }; + const theme = useTheme(); + return ( <> {accessList.length > 0 && ( @@ -99,7 +102,7 @@ const AccessList: React.FC = ({ aria-label="delete" onClick={() => handleDelete(actorData.email)} > - + )} @@ -240,8 +243,6 @@ const ShareModal: React.FC = ({ } fetchSuggestions={fetchSuggestions} /> - - General Access @@ -253,13 +254,13 @@ const ShareModal: React.FC = ({ ) : ( )} @@ -291,15 +292,15 @@ const ShareModal: React.FC = ({ - + - + - Copy Link + Copy Link ({ export const VisibilityIconWrapper = styled(`div`)(({ theme }) => ({ width: '36px', height: '36px', - background: theme.palette.secondary.tabHover, + background: theme.palette.background.hover, borderRadius: '20px', display: 'flex', justifyContent: 'center', @@ -35,14 +35,14 @@ export const FormControlWrapper = styled(FormControl)(() => ({ width: '100%' })); -export const ListWrapper = styled(`div`)(({ theme }) => ({ +export const ListWrapper = styled(`div`)(() => ({ maxHeight: '16rem', - overflowY: 'auto', - color: theme.palette.secondary.text + overflowY: 'auto' })); export const CustomSelect = styled(Select)(() => ({ width: '6rem', + boxShadow: 'none', '&:before': { display: 'none' }, @@ -52,6 +52,12 @@ export const CustomSelect = styled(Select)(() => ({ fontFamily: 'Qanelas Soft, sans-serif', '&.MuiTypography-root': { fontFamily: 'Qanelas Soft, sans-serif' + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none' + }, + '& .MuiSelect-select': { + padding: 0 } })); diff --git a/src/custom/UserSearchField/UserSearchField.tsx b/src/custom/UserSearchField/UserSearchField.tsx index 1023f55e..0b222ae0 100644 --- a/src/custom/UserSearchField/UserSearchField.tsx +++ b/src/custom/UserSearchField/UserSearchField.tsx @@ -89,15 +89,19 @@ const UserSearchField: React.FC = ({ }; const handleInputChange = (_event: React.SyntheticEvent, value: string) => { + console.log('called handle change'); if (value === '') { setOptions([]); setOpen(false); } else { + console.log('Inside else'); setSearchUserLoading(true); fetchSuggestions(value).then((filteredData) => { + console.log('Inside then function', filteredData); setOptions(filteredData); setSearchUserLoading(false); }); + console.log('last'); setError(false); setOpen(true); } From a8561ac12d800b4f39dcb7d68098b1623efa96e8 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Fri, 12 Jul 2024 10:59:59 +0000 Subject: [PATCH 16/16] fix: remove consoles Signed-off-by: Yash Sharma --- src/custom/UserSearchField/UserSearchField.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/custom/UserSearchField/UserSearchField.tsx b/src/custom/UserSearchField/UserSearchField.tsx index 0b222ae0..1023f55e 100644 --- a/src/custom/UserSearchField/UserSearchField.tsx +++ b/src/custom/UserSearchField/UserSearchField.tsx @@ -89,19 +89,15 @@ const UserSearchField: React.FC = ({ }; const handleInputChange = (_event: React.SyntheticEvent, value: string) => { - console.log('called handle change'); if (value === '') { setOptions([]); setOpen(false); } else { - console.log('Inside else'); setSearchUserLoading(true); fetchSuggestions(value).then((filteredData) => { - console.log('Inside then function', filteredData); setOptions(filteredData); setSearchUserLoading(false); }); - console.log('last'); setError(false); setOpen(true); }