-
Notifications
You must be signed in to change notification settings - Fork 402
[upcoming] UIE-9410: Implement the Create Share Group page #13550
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 12 commits
8680aae
d53e3f6
ef5485f
2a8188d
2db02f3
a8323a1
77ec16e
4a803c8
408d904
1befb73
2c3f4dc
66a034c
007e94d
2dbdb37
664d8c3
aabb130
f099e81
e23162c
15f2a0e
1f3b935
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,5 @@ | ||
| --- | ||
| "@linode/manager": Upcoming Features | ||
| --- | ||
|
|
||
| Implement the basic share group create page ([#13550](https://github.com/linode/manager/pull/13550)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { imageSharegroupFactory } from '@linode/utilities'; | ||
| import { userEvent } from '@testing-library/user-event'; | ||
| import React from 'react'; | ||
|
|
||
| import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
|
||
| import { ShareGroupsCreate } from './ShareGroupsCreate'; | ||
|
|
||
| const queryMocks = vi.hoisted(() => ({ | ||
| useCreateShareGroupMutation: vi.fn().mockReturnValue({}), | ||
| useNavigate: vi.fn().mockReturnValue(vi.fn()), | ||
| })); | ||
|
|
||
| vi.mock('@linode/queries', async () => { | ||
| const actual = await vi.importActual('@linode/queries'); | ||
| return { | ||
| ...actual, | ||
| useCreateShareGroupMutation: queryMocks.useCreateShareGroupMutation, | ||
| }; | ||
| }); | ||
|
|
||
| vi.mock('@tanstack/react-router', async () => { | ||
| const actual = await vi.importActual('@tanstack/react-router'); | ||
| return { | ||
| ...actual, | ||
| useNavigate: queryMocks.useNavigate, | ||
| }; | ||
| }); | ||
|
|
||
| describe('ShareGroupsCreate', () => { | ||
| const shareGroupLabel = 'My Share Group'; | ||
| const shareGroupDescription = 'Test Description'; | ||
|
|
||
| let mockNavigate: ReturnType<typeof vi.fn>; | ||
| let mockMutateAsync: ReturnType<typeof vi.fn>; | ||
|
|
||
| beforeEach(() => { | ||
| mockNavigate = vi.fn(); | ||
| mockMutateAsync = vi.fn(); | ||
|
|
||
| queryMocks.useNavigate.mockReturnValue(mockNavigate); | ||
| queryMocks.useCreateShareGroupMutation.mockReturnValue({ | ||
| mutateAsync: mockMutateAsync, | ||
| isLoading: false, | ||
| error: null, | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('should render the form with all fields, titles, and buttons in their default state', () => { | ||
| const { getByRole, getByText } = renderWithTheme(<ShareGroupsCreate />); | ||
|
|
||
| expect(getByText('Share group details')).toBeVisible(); | ||
| expect(getByText('Images')).toBeVisible(); | ||
| expect(getByText('Selected images (0)')).toBeVisible(); | ||
|
|
||
| expect( | ||
| getByText( | ||
| 'Add a name and description for your share group. These details are visible to all group members.' | ||
| ) | ||
| ).toBeVisible(); | ||
|
|
||
| const labelField = getByRole('textbox', { name: /Label/i }); | ||
| expect(labelField).toBeVisible(); | ||
| expect(labelField).toHaveValue(''); | ||
|
|
||
| const descriptionField = getByRole('textbox', { name: /Description/i }); | ||
| expect(descriptionField).toBeVisible(); | ||
| expect(descriptionField).toHaveValue(''); | ||
|
|
||
| const submitButton = getByRole('button', { name: /Create Share Group/i }); | ||
| expect(submitButton).toBeVisible(); | ||
| expect(submitButton).toBeEnabled(); | ||
| }); | ||
|
|
||
| it('should submit the form with valid data', async () => { | ||
| const shareGroup = imageSharegroupFactory.build(); | ||
|
|
||
| mockMutateAsync.mockResolvedValue(shareGroup); | ||
|
|
||
| const { getByRole } = renderWithTheme(<ShareGroupsCreate />); | ||
|
|
||
| const labelField = getByRole('textbox', { name: /Label/i }); | ||
| const descriptionField = getByRole('textbox', { name: /Description/i }); | ||
| const submitButton = getByRole('button', { name: /Create Share Group/i }); | ||
|
|
||
| await userEvent.type(labelField, shareGroupLabel); | ||
| await userEvent.type(descriptionField, shareGroupDescription); | ||
| await userEvent.click(submitButton); | ||
|
|
||
| expect(mockMutateAsync).toHaveBeenCalledWith({ | ||
| label: shareGroupLabel, | ||
| description: shareGroupDescription, | ||
| }); | ||
|
|
||
| expect(mockNavigate).toHaveBeenCalledWith({ | ||
| search: expect.any(Function), | ||
| to: '/images/share-groups', | ||
| }); | ||
| }); | ||
|
|
||
| it('should submit the form with only label (description is optional)', async () => { | ||
| const shareGroup = imageSharegroupFactory.build(); | ||
|
|
||
| mockMutateAsync.mockResolvedValue(shareGroup); | ||
|
|
||
| const { getByRole } = renderWithTheme(<ShareGroupsCreate />); | ||
|
|
||
| const labelField = getByRole('textbox', { name: /Label/i }); | ||
| const submitButton = getByRole('button', { name: /Create Share Group/i }); | ||
|
|
||
| await userEvent.type(labelField, shareGroupLabel); | ||
| await userEvent.click(submitButton); | ||
|
|
||
| expect(mockMutateAsync).toHaveBeenCalledWith({ | ||
| label: shareGroupLabel, | ||
| }); | ||
| }); | ||
|
|
||
| it('should display field-specific errors from API', async () => { | ||
| const apiError = [ | ||
| { | ||
| field: 'label', | ||
| reason: 'Label must be unique', | ||
| }, | ||
| ]; | ||
|
|
||
| mockMutateAsync.mockRejectedValue(apiError); | ||
|
|
||
| const { getByRole, getByText } = renderWithTheme(<ShareGroupsCreate />); | ||
|
|
||
| const labelField = getByRole('textbox', { name: /Label/i }); | ||
| const submitButton = getByRole('button', { name: /Create Share Group/i }); | ||
|
|
||
| await userEvent.type(labelField, 'Duplicate Label'); | ||
| await userEvent.click(submitButton); | ||
|
|
||
| expect(getByText('Label must be unique')).toBeVisible(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { useCreateShareGroupMutation } from '@linode/queries'; | ||
| import { | ||
| Box, | ||
| Button, | ||
| Divider, | ||
| Notice, | ||
| Paper, | ||
| Stack, | ||
| TextField, | ||
| Typography, | ||
| } from '@linode/ui'; | ||
| import { useNavigate } from '@tanstack/react-router'; | ||
| import { useSnackbar } from 'notistack'; | ||
| import * as React from 'react'; | ||
| import { Controller, useForm } from 'react-hook-form'; | ||
|
|
||
| import type { CreateSharegroupPayload } from '@linode/api-v4'; | ||
|
|
||
| export const ShareGroupsCreate = () => { | ||
| const navigate = useNavigate(); | ||
|
|
||
| const { mutateAsync: createShareGroup } = useCreateShareGroupMutation(); | ||
|
|
||
| const { enqueueSnackbar } = useSnackbar(); | ||
|
|
||
| const { control, handleSubmit, setError } = | ||
| useForm<CreateSharegroupPayload>(); | ||
|
|
||
| const selectedImages = []; | ||
|
|
||
| const onSubmit = handleSubmit(async (values) => { | ||
| try { | ||
| await createShareGroup(values); | ||
|
|
||
| enqueueSnackbar('Sharegroup scheduled for creation', { | ||
| variant: 'info', | ||
| }); | ||
|
fabrice-akamai marked this conversation as resolved.
Outdated
|
||
|
|
||
| navigate({ | ||
| search: () => ({}), | ||
| to: '/images/share-groups', | ||
| }); | ||
| } catch (errors) { | ||
| for (const error of errors) { | ||
| if (error.field) { | ||
| setError(error.field, { message: error.reason }); | ||
| } else { | ||
| setError('root', { message: error.reason }); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| return ( | ||
| <form onSubmit={onSubmit}> | ||
| <Paper> | ||
| <Stack spacing={2}> | ||
| <Typography variant="h2">Share group details</Typography> | ||
| <Typography variant="body1"> | ||
| Add a name and description for your share group. These details are | ||
| visible to all group members. | ||
| </Typography> | ||
| <Controller | ||
| control={control} | ||
| name="label" | ||
| render={({ field, fieldState }) => ( | ||
| <TextField | ||
| label="Label" | ||
| noMarginTop | ||
| required | ||
| {...field} | ||
| errorText={fieldState.error?.message} | ||
| onChange={(e) => | ||
| field.onChange( | ||
| e.target.value === '' ? undefined : e.target.value | ||
| ) | ||
| } | ||
| value={field.value ?? ''} | ||
| /> | ||
| )} | ||
| /> | ||
| <Controller | ||
| control={control} | ||
| name="description" | ||
| render={({ field, fieldState }) => ( | ||
| <TextField | ||
| errorText={fieldState.error?.message} | ||
| label="Description" | ||
| multiline | ||
| noMarginTop | ||
| {...field} | ||
| onChange={(e) => | ||
| field.onChange( | ||
| e.target.value === '' ? undefined : e.target.value | ||
| ) | ||
| } | ||
| rows={1} | ||
| value={field.value ?? ''} | ||
| /> | ||
| )} | ||
| /> | ||
|
Comment on lines
+74
to
+93
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.
Contributor
Author
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. Good point, I'll reach out to them about this. |
||
| </Stack> | ||
| <Divider sx={{ marginTop: 4, marginBottom: 4 }} /> | ||
| <Stack spacing={2}> | ||
| <Typography variant="h2">Images</Typography> | ||
| <Notice variant="info">Images table is coming soon...</Notice> | ||
| </Stack> | ||
| <Divider sx={{ marginTop: 4, marginBottom: 4 }} /> | ||
| <Stack spacing={2}> | ||
| <Typography variant="h2"> | ||
| Selected images ({selectedImages.length}) | ||
| </Typography> | ||
| <Notice variant="info">Selected images is coming soon...</Notice> | ||
| </Stack> | ||
| </Paper> | ||
| <Box display="flex" flexWrap="wrap" justifyContent="flex-end" mt={2}> | ||
| <Button buttonType="primary" type="submit"> | ||
| Create Share Group | ||
| </Button> | ||
| </Box> | ||
| </form> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import Grid from '@mui/material/Grid'; | ||
|
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. Let's no do straight imports from
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. Is there an alternative you suggest @abailly-akamai? We do this throughout the codebase and I don't believe we have a
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.
Contributor
Author
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. Haha, no worries. Thanks for clarifying this @dwiley-akamai @abailly-akamai! |
||
| import * as React from 'react'; | ||
|
|
||
| import { DocumentTitleSegment } from 'src/components/DocumentTitle/DocumentTitle'; | ||
| import { LandingHeader } from 'src/components/LandingHeader/LandingHeader'; | ||
|
|
||
| import { ShareGroupsCreate } from './ShareGroupsCreate'; | ||
|
|
||
| export const ShareGroupsCreateContainer = () => { | ||
| return ( | ||
| <> | ||
| <DocumentTitleSegment segment="Create a Share Group" /> | ||
| <LandingHeader | ||
| docsLabel="Docs" | ||
| docsLink="https://techdocs.akamai.com/cloud-computing/docs/image-sharing" | ||
| spacingBottom={4} | ||
| title="Create" | ||
| /> | ||
| <Grid size={12}> | ||
| <ShareGroupsCreate /> | ||
| </Grid> | ||
| </> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { createLazyRoute } from '@tanstack/react-router'; | ||
|
|
||
| import { ShareGroupsCreateContainer } from './ShareGroupsCreateContainer'; | ||
|
|
||
| export const shareGroupsCreateLazyRoute = createLazyRoute( | ||
| '/images/share-groups/create' | ||
| )({ | ||
| component: ShareGroupsCreateContainer, | ||
| }); |


Uh oh!
There was an error while loading. Please reload this page.