Repository navigation
feat(sdk): project delete blockers UI #2041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2770c5
4dc2a24
fd97d2c
0508838
643864d
936eb3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 'use client'; | ||
|
|
||
| import { Image, Menu, Spinner } from '@raystack/apsara'; | ||
| import deleteIcon from '../../../assets/delete.svg'; | ||
|
|
||
| export interface DeleteProjectMenuItemProps { | ||
| isChecking: boolean; | ||
| onClick: () => void; | ||
| 'data-test-id': string; | ||
| } | ||
|
|
||
| export function DeleteProjectMenuItem({ | ||
| isChecking, | ||
| ...props | ||
| }: DeleteProjectMenuItemProps) { | ||
| return ( | ||
| <Menu.Item | ||
| leadingIcon={ | ||
| isChecking ? ( | ||
| <Spinner size={2} /> | ||
| ) : ( | ||
| <Image src={deleteIcon as unknown as string} alt="Delete" width={16} height={16} /> | ||
| ) | ||
| } | ||
| closeOnClick={false} | ||
| disabled={isChecking} | ||
| style={{ color: 'var(--rs-color-foreground-danger-primary)' }} | ||
| {...props} | ||
| > | ||
| Delete project | ||
| </Menu.Item> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,20 +19,29 @@ import { | |
| Menu, | ||
| Select | ||
| } from '@raystack/apsara'; | ||
| import deleteIcon from '../../assets/delete.svg'; | ||
| import { create } from '@bufbuild/protobuf'; | ||
| import { useMutation } from '@connectrpc/connect-query'; | ||
| import { | ||
| CheckProjectDeleteRequestSchema, | ||
| FrontierServiceQueries | ||
| } from '@raystack/proton/frontier'; | ||
| import inboxStackIcon from '../../assets/inbox-stack.svg'; | ||
| import { toastManager } from '@raystack/apsara'; | ||
| import { useFrontier } from '../../contexts/FrontierContext'; | ||
| import { useOrganizationProjects } from '../../hooks/useOrganizationProjects'; | ||
| import { usePermissions } from '../../hooks/usePermissions'; | ||
| import { useDeleteCheck } from '../../hooks/useDeleteCheck'; | ||
| import { AuthTooltipMessage } from '../../utils'; | ||
| import { PERMISSIONS, shouldShowComponent } from '../../../utils'; | ||
| import { ViewContainer } from '../../components/view-container'; | ||
| import { ViewHeader } from '../../components/view-header'; | ||
| import { DeleteBlockedDialogRoot } from '../../components/delete-blocked-dialog'; | ||
| import type { DeleteBlockedPayload } from '../../../shared/types'; | ||
| import { getColumns, type ProjectMenuPayload } from './components/project-columns'; | ||
| import { AddProjectDialog } from './components/add-project-dialog'; | ||
| import { EditProjectDialog, type EditProjectPayload } from './components/edit-project-dialog'; | ||
| import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; | ||
| import { DeleteProjectMenuItem } from './components/delete-project-menu-item'; | ||
| import { AddMemberMenuContent } from './components/add-member-menu'; | ||
| import styles from './projects-view.module.css'; | ||
| import { useTerminology } from '~/client/hooks/useTerminology'; | ||
|
|
@@ -42,10 +51,8 @@ const projectsFilterOptions = [ | |
| { value: 'all-projects', label: 'All Projects' } | ||
| ]; | ||
|
|
||
| const projectMenuHandle = Menu.createHandle<ProjectMenuPayload>(); | ||
| const addProjectDialogHandle = Dialog.createHandle(); | ||
| const editProjectDialogHandle = Dialog.createHandle<EditProjectPayload>(); | ||
| const deleteProjectDialogHandle = AlertDialog.createHandle<DeleteProjectPayload>(); | ||
|
|
||
| export interface ProjectsViewProps { | ||
| title?: string; | ||
|
|
@@ -74,6 +81,30 @@ export function ProjectsView({ | |
|
|
||
| const { activeOrganization: organization } = useFrontier(); | ||
| const t = useTerminology(); | ||
| // Created per mount: a module-level handle keeps a dialog open across | ||
| // remounts (https://github.com/mui/base-ui/issues/4951). | ||
| // TODO @rohanchkrabrty: This is fixed in Base UI v1.7.0. We can update this after Apsara updates | ||
| const [projectMenuHandle] = useState(() => | ||
| Menu.createHandle<ProjectMenuPayload>() | ||
| ); | ||
| const [deleteProjectDialogHandle] = useState(() => | ||
| AlertDialog.createHandle<DeleteProjectPayload>() | ||
| ); | ||
| const [deleteBlockedDialogHandle] = useState(() => | ||
| AlertDialog.createHandle<DeleteBlockedPayload>() | ||
| ); | ||
| const { mutateAsync: checkProjectDelete } = useMutation( | ||
| FrontierServiceQueries.checkProjectDelete | ||
| ); | ||
| const { startDelete, isChecking, handleDeleteError } = useDeleteCheck({ | ||
| entity: 'project', | ||
| check: ({ projectId }) => | ||
| checkProjectDelete( | ||
| create(CheckProjectDeleteRequestSchema, { id: projectId }) | ||
| ), | ||
| confirmDialogHandle: deleteProjectDialogHandle, | ||
| blockedDialogHandle: deleteBlockedDialogHandle | ||
|
Comment on lines
+99
to
+106
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }); | ||
|
|
||
| const resource = `app/organization:${organization?.id}`; | ||
| const listOfPermissionsToCheck = useMemo( | ||
|
|
@@ -136,7 +167,7 @@ export function ProjectsView({ | |
| userAccessOnProject, | ||
| menuHandle: projectMenuHandle | ||
| }), | ||
| [userAccessOnProject] | ||
| [userAccessOnProject, projectMenuHandle] | ||
| ); | ||
|
|
||
| if (hasNoProjects) { | ||
|
|
@@ -303,19 +334,17 @@ export function ProjectsView({ | |
| </Menu.Submenu> | ||
| )} | ||
| {payload?.canDelete && ( | ||
| <Menu.Item | ||
| leadingIcon={<Image src={deleteIcon as unknown as string} alt="Delete" width={16} height={16} />} | ||
| onClick={() => | ||
| deleteProjectDialogHandle.openWithPayload({ | ||
| <DeleteProjectMenuItem | ||
| isChecking={isChecking} | ||
| onClick={async () => { | ||
| await startDelete({ | ||
| projectId: payload.projectId, | ||
| projectName: payload.title | ||
| }) | ||
| } | ||
| }); | ||
| projectMenuHandle.close(); | ||
| }} | ||
|
Comment on lines
+339
to
+345
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| data-test-id="delete-project-dropdown-item" | ||
| style={{ color: 'var(--rs-color-foreground-danger-primary)' }} | ||
| > | ||
| Delete project | ||
| </Menu.Item> | ||
| /> | ||
| )} | ||
| </Menu.Content> | ||
| ); | ||
|
|
@@ -324,7 +353,12 @@ export function ProjectsView({ | |
|
|
||
| <AddProjectDialog handle={addProjectDialogHandle} refetch={refetch} /> | ||
| <EditProjectDialog handle={editProjectDialogHandle} refetch={refetch} /> | ||
| <DeleteProjectDialog handle={deleteProjectDialogHandle} refetch={refetch} /> | ||
| <DeleteProjectDialog | ||
| handle={deleteProjectDialogHandle} | ||
| refetch={refetch} | ||
| onDeleteError={handleDeleteError} | ||
| /> | ||
| <DeleteBlockedDialogRoot handle={deleteBlockedDialogHandle} /> | ||
| </ViewContainer> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: raystack/frontier
Length of output: 6373
Script executed:
Repository: raystack/frontier
Length of output: 12676
Script executed:
Repository: raystack/frontier
Length of output: 5440
Key the details view by
projectIdso stale delete checks cannot reach a new project.ProjectDetailsViewkeeps its state whenprojectIdchanges. If a check for project A resolves after navigation to project B, the confirm dialog opens with A's payload. Confirming from B's page deletes A. Remount the view whenprojectIdchanges, for example by keying it byprojectIdinside the SDK export.