Skip to content

Commit

Permalink
Improve order events history (#5371)
Browse files Browse the repository at this point in the history
* Use orderNoteAdd mutation instead of orderAddNote

* Show when note is updated with reloaded and note id

* Add handling note update

* Add loading state handling

* Extract messages

* Fix imports and tests

* Add tests

* Add changeset

* Fix typing

* Improve typing

* Add subtitle with docs link

* Extract messages
  • Loading branch information
poulch authored Jan 29, 2025
1 parent d1c02d1 commit 0e3109c
Show file tree
Hide file tree
Showing 21 changed files with 557 additions and 126 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-points-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

You can now edit note in order details. Notes in order details now show id of note, id of related note and type of note "added" or "updated"
21 changes: 21 additions & 0 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@
"context": "channels alphabetically title",
"string": "Channels from A to Z"
},
"/n+NRO": {
"string": "Note id"
},
"/nZy6A": {
"context": "page subtitle",
"string": "Issue refund"
Expand Down Expand Up @@ -1711,6 +1714,9 @@
"context": "order number label",
"string": "Order number"
},
"9Th87u": {
"string": "Note successfully updated"
},
"9Tl/bT": {
"context": "export items as spreadsheet",
"string": "Spreadsheet for Excel, Numbers etc."
Expand Down Expand Up @@ -6496,6 +6502,9 @@
"context": "edit tracking button",
"string": "Edit tracking"
},
"dVSBW6": {
"string": "Related note id"
},
"dVk241": {
"string": "product configurations"
},
Expand Down Expand Up @@ -6699,6 +6708,9 @@
"context": "gift card history message",
"string": "Gift card expiry date was updated"
},
"fM7xZh": {
"string": "updated"
},
"fNFEkh": {
"string": "No of Rows:"
},
Expand Down Expand Up @@ -7057,6 +7069,9 @@
"context": "docs link label",
"string": "Learn more..."
},
"hniz8Z": {
"string": "here"
},
"ho75Lr": {
"context": "status label deactivated",
"string": "Deactivated"
Expand Down Expand Up @@ -8313,6 +8328,9 @@
"context": "header",
"string": "Add Value to Authorization Field"
},
"qD1kvF": {
"string": "The timeline below shows the history of all events related to this order. Each entry represents a single event along with its content or readable description. For more information regarding order events, you can find {link}."
},
"qDfaDI": {
"string": "No app or plugin is configured to handle requested transaction action"
},
Expand Down Expand Up @@ -9367,6 +9385,9 @@
"context": "gift card settings header",
"string": "Gift Cards Settings"
},
"xJEaxW": {
"string": "added"
},
"xJQX5t": {
"string": "No staff members found"
},
Expand Down
84 changes: 83 additions & 1 deletion src/components/Timeline/TimelineNote.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OrderEventFragment } from "@dashboard/graphql/types.generated";
import Wrapper from "@test/wrapper";
import { render, screen } from "@testing-library/react";
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";

import TimelineNote from "./TimelineNote";
Expand Down Expand Up @@ -142,4 +143,85 @@ describe("TimelineNote", () => {
expect(initials).toBeNull();
expect(avatar).toBeInTheDocument();
});

it("renders note id and refer id", () => {
// Arrange
const noteId = "T3JkZXJFdmVudDozNDM3";
const noteRelatedId = "T3JkZXJFdmVudDozNDQx";
const mockedUser = {
avatar: null,
id: "1",
email: "[email protected]",
firstName: "Test",
lastName: "User",
__typename: "User",
} satisfies OrderEventFragment["user"];

// Act
render(
<TimelineNote
app={null}
user={mockedUser}
date={wrapperFriendlyDate}
message="Note"
hasPlainDate={false}
id={noteId}
relatedId={noteRelatedId}
/>,
{ wrapper: Wrapper },
);

// Assert
expect(screen.getByText("Test User")).toBeInTheDocument();
expect(screen.getByText("Note")).toBeInTheDocument();
expect(screen.getByText("TU")).toBeInTheDocument();
expect(screen.getByText("a few seconds ago")).toBeInTheDocument();
expect(screen.getByText(`Note id: ${noteId}`)).toBeInTheDocument();
expect(screen.getByText(new RegExp(noteRelatedId))).toBeInTheDocument();
});

it("should edit note", async () => {
// Arrange
const noteId = "T3JkZXJFdmVudDozNDM3";
const noteRelatedId = "T3JkZXJFdmVudDozNDQx";
const onNoteUpdate = jest.fn();
const onNoteUpdateLoading = false;
const mockedUser = {
avatar: null,
id: "1",
email: "[email protected]",
firstName: "Test",
lastName: "User",
__typename: "User",
} satisfies OrderEventFragment["user"];

render(
<TimelineNote
app={null}
user={mockedUser}
date={wrapperFriendlyDate}
message="Note"
hasPlainDate={false}
id={noteId}
relatedId={noteRelatedId}
onNoteUpdate={onNoteUpdate}
onNoteUpdateLoading={onNoteUpdateLoading}
/>,
{ wrapper: Wrapper },
);

// Act
await act(async () => {
await userEvent.click(screen.getByTestId("edit-note"));
});

await act(async () => {
await userEvent.clear(screen.getByRole("textbox"));
await userEvent.type(screen.getByRole("textbox"), "New note");
await userEvent.click(screen.getByRole("button", { name: /save/i }));
});

// Assert
expect(onNoteUpdate).toHaveBeenCalledWith(noteId, "New note");
});
});
132 changes: 80 additions & 52 deletions src/components/Timeline/TimelineNote.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,13 @@
import { GiftCardEventsQuery, OrderEventFragment } from "@dashboard/graphql";
import { getUserInitials, getUserName } from "@dashboard/misc";
import { makeStyles } from "@saleor/macaw-ui";
import { Text } from "@saleor/macaw-ui-next";
import React from "react";
import { Box, Button, EditIcon, Text } from "@saleor/macaw-ui-next";
import React, { useState } from "react";
import { FormattedMessage } from "react-intl";

import { DashboardCard } from "../Card";
import { DateTime } from "../Date";
import { UserAvatar } from "../UserAvatar";

const useStyles = makeStyles(
theme => ({
avatar: {
left: -40,
position: "absolute",
top: 0,
},
root: {
position: "relative",
},
title: {
"& p": {
fontSize: "14px",
},
alignItems: "center",
display: "flex",
justifyContent: "space-between",
marginBottom: theme.spacing(),
},
}),
{ name: "TimelineNote" },
);
import { TimelineNoteEdit } from "./TimelineNoteEdit";

type TimelineAppType =
| NonNullable<GiftCardEventsQuery["giftCard"]>["events"][0]["app"]
Expand All @@ -41,6 +19,10 @@ interface TimelineNoteProps {
user: OrderEventFragment["user"];
app: TimelineAppType;
hasPlainDate?: boolean;
id?: string;
relatedId?: string;
onNoteUpdate?: (id: string, message: string) => Promise<unknown>;
onNoteUpdateLoading?: boolean;
}

interface NoteMessageProps {
Expand All @@ -66,7 +48,7 @@ const TimelineAvatar = ({
}: {
user: OrderEventFragment["user"];
app: TimelineAppType;
className: string;
className?: string;
}) => {
if (user) {
return (
Expand All @@ -93,38 +75,84 @@ export const TimelineNote: React.FC<TimelineNoteProps> = ({
message,
hasPlainDate,
app,
id,
relatedId,
onNoteUpdate,
onNoteUpdateLoading,
}) => {
const classes = useStyles();

const userDisplayName = getUserName(user, true) ?? app?.name;
const [showEdit, setShowEdit] = useState(false);

return (
<div className={classes.root}>
<TimelineAvatar user={user} app={app} className={classes.avatar} />
<div className={classes.title}>
<Box position="relative">
<Box position="absolute" top={0} __left={-40}>
<TimelineAvatar user={user} app={app} />
</Box>
<Box marginBottom={2} display="flex" alignItems="center" justifyContent="space-between">
<Text size={3}>{userDisplayName}</Text>
<Text size={3} color="default2" whiteSpace="nowrap">
<Text size={3} color="default2" display="flex" gap={1} whiteSpace="nowrap">
{relatedId ? (
<FormattedMessage defaultMessage="updated" id="fM7xZh" />
) : (
<FormattedMessage defaultMessage="added" id="xJEaxW" />
)}
<DateTime date={date} plain={hasPlainDate} />
</Text>
</div>
<DashboardCard
marginBottom={6}
position="relative"
boxShadow="defaultOverlay"
backgroundColor="default1"
>
<DashboardCard.Content
wordBreak="break-all"
borderRadius={2}
borderStyle="solid"
borderWidth={1}
borderColor="default1"
padding={4}
>
<NoteMessage message={message} />
</DashboardCard.Content>
</DashboardCard>
</div>
</Box>

{showEdit && id ? (
<TimelineNoteEdit
id={id}
note={message!}
onSubmit={onNoteUpdate!}
loading={onNoteUpdateLoading!}
onCancel={() => setShowEdit(false)}
/>
) : (
<>
<DashboardCard marginBottom={2} position="relative" backgroundColor="default1">
<DashboardCard.Content
wordBreak="break-all"
borderRadius={2}
borderStyle="solid"
borderWidth={1}
borderColor="default1"
padding={4}
paddingRight={2}
display="flex"
justifyContent="space-between"
gap={3}
>
<NoteMessage message={message} />
{onNoteUpdate && (
<Button
data-test-id="edit-note"
variant="tertiary"
size="small"
onClick={() => {
setShowEdit(true);
}}
icon={<EditIcon size="small" />}
/>
)}
</DashboardCard.Content>
</DashboardCard>

<Box marginBottom={6} display="flex" justifyContent="space-between" alignItems="center">
{id && (
<Text size={2} color="defaultDisabled">
<FormattedMessage defaultMessage="Note id" id="/n+NRO" />: {id}
</Text>
)}
{relatedId && (
<Text size={2} color="defaultDisabled">
<FormattedMessage defaultMessage="Related note id" id="dVSBW6" /> : {relatedId}
</Text>
)}
</Box>
</>
)}
</Box>
);
};
TimelineNote.displayName = "TimelineNote";
Expand Down
58 changes: 58 additions & 0 deletions src/components/Timeline/TimelineNoteEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ConfirmButton } from "@dashboard/components/ConfirmButton";
import { buttonMessages } from "@dashboard/intl";
import { Box, Button, Textarea } from "@saleor/macaw-ui-next";
import React from "react";
import { useForm } from "react-hook-form";
import { FormattedMessage } from "react-intl";

interface TimelineNoteEditProps {
id: string;
note: string;
loading: boolean;
onSubmit: (id: string, message: string) => Promise<unknown>;
onCancel: () => void;
}

interface TimelineNoteEditData {
note: string;
}

export const TimelineNoteEdit = ({
onCancel,
note,
onSubmit,
id,
loading,
}: TimelineNoteEditProps) => {
const { handleSubmit, register } = useForm<TimelineNoteEditData>({
defaultValues: {
note,
},
});

const submitHandler = async (data: TimelineNoteEditData) => {
await onSubmit(id, data.note);
onCancel();
};

return (
<Box as="form" marginBottom={6} width="100%" onSubmit={handleSubmit(submitHandler)}>
<Textarea autoFocus fontSize={4} paddingY={2.5} paddingX={4} rows={5} {...register("note")} />
<Box marginTop={3} display="flex" alignItems="center" justifyContent="flex-end" gap={2}>
<Button disabled={loading} variant="secondary" onClick={onCancel}>
<FormattedMessage {...buttonMessages.cancel} />
</Button>
<ConfirmButton
data-test-id="save-note"
disabled={loading}
transitionState={loading ? "loading" : "default"}
variant="primary"
type="button"
onClick={handleSubmit(submitHandler)}
>
<FormattedMessage {...buttonMessages.save} />
</ConfirmButton>
</Box>
</Box>
);
};
Loading

0 comments on commit 0e3109c

Please sign in to comment.