Skip to content
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

feat(web): add support for multi-group ordering #1158

Merged
merged 6 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion web/e2e/project/schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { closeNotification } from "@reearth-cms/e2e/common/notification";
import { expect, test } from "@reearth-cms/e2e/utils";

import { handleFieldForm } from "./utils/field";
import { crudGroup } from "./utils/group";
import { createGroup, crudGroup } from "./utils/group";
import { createModel, crudModel } from "./utils/model";
import { createProject, deleteProject } from "./utils/project";

Expand Down Expand Up @@ -97,6 +97,41 @@ test("Group creating from adding field has succeeded", async ({ page }) => {
await page.getByRole("button", { name: "Cancel" }).click();
});

test("Group reordering has succeeded", async ({ page }) => {
await createGroup(page, "group1", "group1");
await createGroup(page, "group2", "group2");
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(0),
).toContainText("group1");
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(1),
).toContainText("group2");
await page
.getByRole("main")
.getByRole("menu")
.last()
.getByRole("menuitem")
.nth(1)
.dragTo(page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(0));
await closeNotification(page);
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(0),
).toContainText("group2");
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(1),
).toContainText("group1");
await createGroup(page, "group3", "group3");
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(0),
).toContainText("group2");
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(1),
).toContainText("group1");
await expect(
page.getByRole("main").getByRole("menu").last().getByRole("menuitem").nth(2),
).toContainText("group3");
});

test("Text field CRUD has succeeded", async ({ page }) => {
await createModel(page);
await page.locator("li").filter({ hasText: "Text" }).locator("div").first().click();
Expand Down
10 changes: 5 additions & 5 deletions web/e2e/project/utils/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { Page } from "@playwright/test";
import { closeNotification } from "@reearth-cms/e2e/common/notification";
import { expect } from "@reearth-cms/e2e/utils";

export async function createGroup(page: Page) {
export async function createGroup(page: Page, name = "e2e group name", key = "e2e-group-key") {
await page.getByText("Schema").first().click();
await page.getByRole("button", { name: "plus Add" }).last().click();
await page.getByLabel("New Group").locator("#name").click();
await page.getByLabel("New Group").locator("#name").fill("e2e group name");
await page.getByLabel("New Group").locator("#name").fill(name);
await page.getByLabel("New Group").locator("#key").click();
await page.getByLabel("New Group").locator("#key").fill("e2e-group-key");
await page.getByLabel("New Group").locator("#key").fill(key);
await page.getByRole("button", { name: "OK" }).click();
await closeNotification(page);
await expect(page.getByTitle("e2e group name")).toBeVisible();
await expect(page.getByText("#e2e-group-key")).toBeVisible();
await expect(page.getByTitle(name, { exact: true })).toBeVisible();
await expect(page.getByText(`#${key}`)).toBeVisible();
}

const updateGroupName = "new e2e group name";
Expand Down
3 changes: 3 additions & 0 deletions web/src/components/molecules/Model/ModelsList/Groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Props = {
onClose: () => void;
onCreate: (data: { name: string; description: string; key: string }) => Promise<void>;
onGroupSelect?: (groupId: string) => void;
onUpdateGroupsOrder: (groupIds: string[]) => void;
};

const Groups: React.FC<Props> = ({
Expand All @@ -25,6 +26,7 @@ const Groups: React.FC<Props> = ({
onClose,
onCreate,
onGroupSelect,
onUpdateGroupsOrder,
}) => {
return (
<>
Expand All @@ -34,6 +36,7 @@ const Groups: React.FC<Props> = ({
collapsed={collapsed}
onGroupSelect={onGroupSelect}
onModalOpen={onModalOpen}
onUpdateGroupsOrder={onUpdateGroupsOrder}
/>
<FormModal
open={open}
Expand Down
51 changes: 36 additions & 15 deletions web/src/components/molecules/Model/ModelsList/GroupsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from "@emotion/styled";
import { useCallback, useMemo } from "react";
import ReactDragListView from "react-drag-listview";

import Button from "@reearth-cms/components/atoms/Button";
import Icon from "@reearth-cms/components/atoms/Icon";
Expand All @@ -13,6 +14,7 @@ type Props = {
collapsed?: boolean;
onModalOpen: () => void;
onGroupSelect?: (groupId: string) => void;
onUpdateGroupsOrder: (groupIds: string[]) => void;
};

const GroupsList: React.FC<Props> = ({
Expand All @@ -21,6 +23,7 @@ const GroupsList: React.FC<Props> = ({
collapsed,
onModalOpen,
onGroupSelect,
onUpdateGroupsOrder,
}) => {
const t = useT();

Expand All @@ -35,14 +38,16 @@ const GroupsList: React.FC<Props> = ({

const items = useMemo(
() =>
groups?.map(group => ({
label: (
<div ref={group.id === selectedKey ? scrollToSelected : undefined}>
{collapsed ? <Icon icon="dot" /> : group.name}
</div>
),
key: group.id,
})),
groups
?.sort((a, b) => a.order - b.order)
.map(group => ({
label: (
<div ref={group.id === selectedKey ? scrollToSelected : undefined}>
{collapsed ? <Icon icon="dot" /> : group.name}
</div>
),
key: group.id,
})),
[collapsed, groups, scrollToSelected, selectedKey],
);

Expand All @@ -53,6 +58,17 @@ const GroupsList: React.FC<Props> = ({
[onGroupSelect],
);

const onDragEnd = useCallback(
(fromIndex: number, toIndex: number) => {
if (toIndex < 0 || !groups) return;
const [removed] = groups.splice(fromIndex, 1);
groups.splice(toIndex, 0, removed);
const groupIds = groups.map(group => group.id);
onUpdateGroupsOrder(groupIds);
},
[groups, onUpdateGroupsOrder],
);

return (
<SchemaStyledMenu>
{collapsed ? (
Expand All @@ -68,13 +84,18 @@ const GroupsList: React.FC<Props> = ({
</Header>
)}
<MenuWrapper>
<StyledMenu
selectedKeys={selectedKeys}
mode={collapsed ? "vertical" : "inline"}
collapsed={collapsed}
items={items}
onClick={handleClick}
/>
<ReactDragListView
nodeSelector=".ant-menu-item"
lineClassName="dragLine"
onDragEnd={(fromIndex, toIndex) => onDragEnd(fromIndex, toIndex)}>
<StyledMenu
selectedKeys={selectedKeys}
mode={collapsed ? "vertical" : "inline"}
collapsed={collapsed}
items={items}
onClick={handleClick}
/>
</ReactDragListView>
</MenuWrapper>
</SchemaStyledMenu>
);
Expand Down
1 change: 1 addition & 0 deletions web/src/components/molecules/Schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export type Group = {
description: string;
key: string;
schema: Schema;
order: number;
};

export type ModelFormValues = {
Expand Down
1 change: 1 addition & 0 deletions web/src/components/organisms/DataConverters/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const fromGraphQLGroup = (group: Maybe<GQLGroup>): Group | undefined => {
name: group.name,
description: group.description,
key: group.key,
order: group.order,
schema: {
id: group.schema?.id,
fields: group.schema?.fields.map(
Expand Down
22 changes: 22 additions & 0 deletions web/src/components/organisms/Project/ModelsMenu/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useCreateModelMutation,
useUpdateModelsOrderMutation,
useCreateGroupMutation,
useUpdateGroupsOrderMutation,
useCheckModelKeyAvailabilityLazyQuery,
useCheckGroupKeyAvailabilityLazyQuery,
Model as GQLModel,
Expand Down Expand Up @@ -186,6 +187,26 @@ export default ({ modelId }: Params) => {
[currentWorkspace?.id, projectId, createNewGroup, navigate, t],
);

const [updateGroupsOrder] = useUpdateGroupsOrderMutation({
refetchQueries: ["GetGroups"],
});

const handleUpdateGroupsOrder = useCallback(
async (groupIds: string[]) => {
const group = await updateGroupsOrder({
variables: {
groupIds,
},
});
if (group.errors) {
Notification.error({ message: t("Failed to update groups order.") });
return;
}
Notification.success({ message: t("Successfully updated groups order!") });
},
[updateGroupsOrder, t],
);

return {
models,
groups,
Expand All @@ -200,5 +221,6 @@ export default ({ modelId }: Params) => {
handleGroupCreate,
handleGroupKeyCheck,
handleUpdateModelsOrder,
handleUpdateGroupsOrder,
};
};
2 changes: 2 additions & 0 deletions web/src/components/organisms/Project/ModelsMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const ModelsMenu: React.FC<Props> = ({
handleModelKeyCheck,
handleGroupKeyCheck,
handleUpdateModelsOrder,
handleUpdateGroupsOrder,
} = useHooks({
modelId: selectedSchemaType === "model" ? schemaId : undefined,
});
Expand Down Expand Up @@ -74,6 +75,7 @@ const ModelsMenu: React.FC<Props> = ({
onGroupKeyCheck={handleGroupKeyCheck}
onClose={handleGroupModalClose}
onCreate={handleGroupCreate}
onUpdateGroupsOrder={handleUpdateGroupsOrder}
/>
)}
</ModelListBody>
Expand Down
63 changes: 62 additions & 1 deletion web/src/gql/graphql-client-api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ export type Group = Node & {
id: Scalars['ID']['output'];
key: Scalars['String']['output'];
name: Scalars['String']['output'];
order: Scalars['Int']['output'];
project: Project;
projectId: Scalars['ID']['output'];
schema: Schema;
Expand All @@ -534,6 +535,11 @@ export type GroupPayload = {
group: Group;
};

export type GroupsPayload = {
__typename?: 'GroupsPayload';
groups: Array<Group>;
};

export type Integration = Node & {
__typename?: 'Integration';
config?: Maybe<IntegrationConfig>;
Expand Down Expand Up @@ -788,6 +794,7 @@ export type Mutation = {
updateField?: Maybe<FieldPayload>;
updateFields?: Maybe<FieldsPayload>;
updateGroup?: Maybe<GroupPayload>;
updateGroupsOrder?: Maybe<GroupsPayload>;
updateIntegration?: Maybe<IntegrationPayload>;
updateIntegrationOfWorkspace?: Maybe<UpdateMemberOfWorkspacePayload>;
updateItem?: Maybe<ItemPayload>;
Expand Down Expand Up @@ -1014,6 +1021,11 @@ export type MutationUpdateGroupArgs = {
};


export type MutationUpdateGroupsOrderArgs = {
input: UpdateGroupsOrderInput;
};


export type MutationUpdateIntegrationArgs = {
input: UpdateIntegrationInput;
};
Expand Down Expand Up @@ -1900,6 +1912,10 @@ export type UpdateGroupInput = {
name?: InputMaybe<Scalars['String']['input']>;
};

export type UpdateGroupsOrderInput = {
groupIds: Array<Scalars['ID']['input']>;
};

export type UpdateIntegrationInput = {
description?: InputMaybe<Scalars['String']['input']>;
integrationId: Scalars['ID']['input'];
Expand Down Expand Up @@ -2320,7 +2336,7 @@ export type GetGroupsQueryVariables = Exact<{
}>;


export type GetGroupsQuery = { __typename?: 'Query', groups: Array<{ __typename?: 'Group', id: string, name: string, key: string } | null> };
export type GetGroupsQuery = { __typename?: 'Query', groups: Array<{ __typename?: 'Group', id: string, name: string, key: string, order: number } | null> };

export type GetGroupQueryVariables = Exact<{
id: Scalars['ID']['input'];
Expand Down Expand Up @@ -2371,6 +2387,13 @@ export type ModelsByGroupQueryVariables = Exact<{

export type ModelsByGroupQuery = { __typename?: 'Query', modelsByGroup: Array<{ __typename?: 'Model', name: string } | null> };

export type UpdateGroupsOrderMutationVariables = Exact<{
groupIds: Array<Scalars['ID']['input']> | Scalars['ID']['input'];
}>;


export type UpdateGroupsOrderMutation = { __typename?: 'Mutation', updateGroupsOrder?: { __typename?: 'GroupsPayload', groups: Array<{ __typename?: 'Group', id: string }> } | null };

export type CreateIntegrationMutationVariables = Exact<{
name: Scalars['String']['input'];
description?: InputMaybe<Scalars['String']['input']>;
Expand Down Expand Up @@ -3860,6 +3883,7 @@ export const GetGroupsDocument = gql`
id
name
key
order
}
}
`;
Expand Down Expand Up @@ -4187,6 +4211,43 @@ export type ModelsByGroupQueryHookResult = ReturnType<typeof useModelsByGroupQue
export type ModelsByGroupLazyQueryHookResult = ReturnType<typeof useModelsByGroupLazyQuery>;
export type ModelsByGroupSuspenseQueryHookResult = ReturnType<typeof useModelsByGroupSuspenseQuery>;
export type ModelsByGroupQueryResult = Apollo.QueryResult<ModelsByGroupQuery, ModelsByGroupQueryVariables>;
export const UpdateGroupsOrderDocument = gql`
mutation UpdateGroupsOrder($groupIds: [ID!]!) {
updateGroupsOrder(input: {groupIds: $groupIds}) {
groups {
... on Group {
id
}
}
}
}
`;
export type UpdateGroupsOrderMutationFn = Apollo.MutationFunction<UpdateGroupsOrderMutation, UpdateGroupsOrderMutationVariables>;

/**
* __useUpdateGroupsOrderMutation__
*
* To run a mutation, you first call `useUpdateGroupsOrderMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateGroupsOrderMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateGroupsOrderMutation, { data, loading, error }] = useUpdateGroupsOrderMutation({
* variables: {
* groupIds: // value for 'groupIds'
* },
* });
*/
export function useUpdateGroupsOrderMutation(baseOptions?: Apollo.MutationHookOptions<UpdateGroupsOrderMutation, UpdateGroupsOrderMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateGroupsOrderMutation, UpdateGroupsOrderMutationVariables>(UpdateGroupsOrderDocument, options);
}
export type UpdateGroupsOrderMutationHookResult = ReturnType<typeof useUpdateGroupsOrderMutation>;
export type UpdateGroupsOrderMutationResult = Apollo.MutationResult<UpdateGroupsOrderMutation>;
export type UpdateGroupsOrderMutationOptions = Apollo.BaseMutationOptions<UpdateGroupsOrderMutation, UpdateGroupsOrderMutationVariables>;
export const CreateIntegrationDocument = gql`
mutation CreateIntegration($name: String!, $description: String, $logoUrl: URL!, $type: IntegrationType!) {
createIntegration(
Expand Down
Loading
Loading