first load
Some checks failed
Build, Push, Publish / Build & Release (push) Failing after 2s
Sync Repo / sync (push) Failing after 2s

This commit is contained in:
2025-12-16 04:40:00 -03:00
parent 9f33a94e0e
commit 6fa41a771d
856 changed files with 70411 additions and 1 deletions

View File

@@ -0,0 +1,163 @@
import { render } from "../test/render";
import { screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import ApprovalRequestListPage from "./ApprovalRequestListPage";
import { useSearchApprovalRequests } from "./hooks/useSearchApprovalRequests";
jest.mock(
"@zendeskgarden/svg-icons/src/16/search-stroke.svg",
() => "svg-mock"
);
jest.mock("./hooks/useSearchApprovalRequests");
const mockUseSearchApprovalRequests = useSearchApprovalRequests as jest.Mock;
const mockApprovalRequests = [
{
id: 123,
subject: "Hardware request",
requester_name: "Jane Doe",
created_by_name: "John Doe",
created_at: "2024-02-20T10:00:00Z",
status: "active",
},
{
id: 456,
subject: "Software license",
requester_name: "Jane Smith",
created_by_name: "Bob Smith",
created_at: "2024-02-19T15:00:00Z",
status: "approved",
},
];
describe("ApprovalRequestListPage", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("renders the loading state initially", () => {
mockUseSearchApprovalRequests.mockReturnValue({
approvalRequests: [],
errorFetchingApprovalRequests: null,
approvalRequestStatus: "any",
setApprovalRequestStatus: jest.fn(),
isLoading: true,
});
render(
<ApprovalRequestListPage baseLocale="en-US" helpCenterPath="/hc/en-us" />
);
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
it("renders the approval requests list page with data", () => {
mockUseSearchApprovalRequests.mockReturnValue({
approvalRequests: mockApprovalRequests,
errorFetchingApprovalRequests: null,
approvalRequestStatus: "any",
setApprovalRequestStatus: jest.fn(),
isLoading: false,
});
render(
<ApprovalRequestListPage baseLocale="en-US" helpCenterPath="/hc/en-us" />
);
expect(screen.getByText("Approval requests")).toBeInTheDocument();
expect(screen.getByText("Hardware request")).toBeInTheDocument();
expect(screen.getByText("Software license")).toBeInTheDocument();
});
it("renders the empty state when there are no approval requests", () => {
mockUseSearchApprovalRequests.mockReturnValue({
approvalRequests: [],
errorFetchingApprovalRequests: null,
approvalRequestStatus: "any",
setApprovalRequestStatus: jest.fn(),
isLoading: false,
});
render(
<ApprovalRequestListPage baseLocale="en-US" helpCenterPath="/hc/en-us" />
);
expect(screen.getByText("No approval requests found.")).toBeInTheDocument();
});
it("filters the approval requests by search term", async () => {
const user = userEvent.setup();
mockUseSearchApprovalRequests.mockReturnValue({
approvalRequests: mockApprovalRequests,
errorFetchingApprovalRequests: null,
approvalRequestStatus: "any",
setApprovalRequestStatus: jest.fn(),
isLoading: false,
});
render(
<ApprovalRequestListPage baseLocale="en-US" helpCenterPath="/hc/en-us" />
);
const searchInput = screen.getByPlaceholderText(
/search approval requests/i
);
await user.type(searchInput, "Hardware");
waitFor(() => {
expect(screen.getByText("Hardware request")).toBeInTheDocument();
expect(screen.queryByText("Software license")).not.toBeInTheDocument();
});
});
it("sorts the approval requests by the sent on date", async () => {
const user = userEvent.setup();
mockUseSearchApprovalRequests.mockReturnValue({
approvalRequests: mockApprovalRequests,
errorFetchingApprovalRequests: null,
approvalRequestStatus: "any",
setApprovalRequestStatus: jest.fn(),
isLoading: false,
});
render(
<ApprovalRequestListPage baseLocale="en-US" helpCenterPath="/hc/en-us" />
);
const createdHeader = screen.getByText("Sent on");
await user.click(createdHeader);
// Wait for re-render after sort
await waitFor(() => {
const rows = screen.getAllByRole("row").slice(1); // Skip header row
expect(rows[0]).toHaveTextContent("Software license");
expect(rows[1]).toHaveTextContent("Hardware request");
});
});
it("throws an error when the request fails", () => {
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
const error = new Error("Failed to fetch");
mockUseSearchApprovalRequests.mockReturnValue({
approvalRequests: [],
errorFetchingApprovalRequests: error,
approvalRequestStatus: "any",
setApprovalRequestStatus: jest.fn(),
isLoading: false,
});
expect(() =>
render(
<ApprovalRequestListPage
baseLocale="en-US"
helpCenterPath="/hc/en-us"
/>
)
).toThrow("Failed to fetch");
consoleSpy.mockRestore();
});
});

View File

@@ -0,0 +1,106 @@
import { memo, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { XXL } from "@zendeskgarden/react-typography";
import { Spinner } from "@zendeskgarden/react-loaders";
import { useSearchApprovalRequests } from "./hooks/useSearchApprovalRequests";
import ApprovalRequestListFilters from "./components/approval-request-list/ApprovalRequestListFilters";
import ApprovalRequestListTable from "./components/approval-request-list/ApprovalRequestListTable";
import NoApprovalRequestsText from "./components/approval-request-list/NoApprovalRequestsText";
const Container = styled.div`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.space.lg};
margin-top: ${(props) => props.theme.space.xl}; /* 40px */
`;
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
export interface ApprovalRequestListPageProps {
baseLocale: string;
helpCenterPath: string;
}
type SortDirection = "asc" | "desc" | undefined;
function ApprovalRequestListPage({
baseLocale,
helpCenterPath,
}: ApprovalRequestListPageProps) {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState("");
const [sortDirection, setSortDirection] = useState<SortDirection>(undefined);
const {
approvalRequests,
errorFetchingApprovalRequests: error,
approvalRequestStatus,
setApprovalRequestStatus,
isLoading,
} = useSearchApprovalRequests();
const sortedAndFilteredApprovalRequests = useMemo(() => {
let results = [...approvalRequests];
// Apply search filter
if (searchTerm) {
const term = searchTerm.toLowerCase();
results = results.filter((request) =>
request.subject.toLowerCase().includes(term)
);
}
// Apply sorting
if (sortDirection) {
results.sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return sortDirection === "asc" ? dateA - dateB : dateB - dateA;
});
}
return results;
}, [approvalRequests, searchTerm, sortDirection]);
if (error) {
throw error;
}
if (isLoading) {
return (
<LoadingContainer>
<Spinner size="64" />
</LoadingContainer>
);
}
return (
<Container>
<XXL isBold>
{t("approval-requests.list.header", "Approval requests")}
</XXL>
<ApprovalRequestListFilters
approvalRequestStatus={approvalRequestStatus}
setApprovalRequestStatus={setApprovalRequestStatus}
setSearchTerm={setSearchTerm}
/>
{approvalRequests.length === 0 ? (
<NoApprovalRequestsText />
) : (
<ApprovalRequestListTable
approvalRequests={sortedAndFilteredApprovalRequests}
baseLocale={baseLocale}
helpCenterPath={helpCenterPath}
sortDirection={sortDirection}
onSortChange={setSortDirection}
/>
)}
</Container>
);
}
export default memo(ApprovalRequestListPage);

View File

@@ -0,0 +1,179 @@
import { screen } from "@testing-library/react";
import { render } from "../test/render";
import ApprovalRequestPage from "./ApprovalRequestPage";
import { useApprovalRequest } from "./hooks/useApprovalRequest";
import type { ApprovalRequest } from "./types";
jest.mock("@zendeskgarden/svg-icons/src/16/headset-fill.svg", () => "svg-mock");
jest.mock(
"@zendeskgarden/svg-icons/src/12/circle-sm-fill.svg",
() => "svg-mock"
);
jest.mock("./hooks/useApprovalRequest");
const mockUseApprovalRequest = useApprovalRequest as jest.Mock;
const mockUserAvatarUrl = "https://example.com/avatar.jpg";
const mockUserName = "Test User";
const mockApprovalRequest: ApprovalRequest = {
id: "123",
subject: "Test Request",
message: "Please approve this request",
status: "active",
created_at: "2024-02-20T10:00:00Z",
created_by_user: {
id: 1,
name: "Creator User",
photo: { content_url: null },
},
assignee_user: {
id: 2,
name: "Approver User",
photo: { content_url: null },
},
decided_at: null,
decisions: [],
withdrawn_reason: null,
ticket_details: {
id: "T123",
priority: "normal",
requester: {
id: 1,
name: "Creator User",
photo: { content_url: null },
},
custom_fields: [],
},
};
const baseProps = {
approvalWorkflowInstanceId: "456",
approvalRequestId: "123",
baseLocale: "en-US",
helpCenterPath: "/hc/en-us",
organizations: [],
userAvatarUrl: mockUserAvatarUrl,
userName: mockUserName,
};
describe("ApprovalRequestPage", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("renders the loading state initially", () => {
mockUseApprovalRequest.mockReturnValue({
approvalRequest: null,
setApprovalRequest: jest.fn(),
isLoading: true,
errorFetchingApprovalRequest: null,
});
render(<ApprovalRequestPage {...baseProps} userId={1} />);
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
it("renders the approval request details", async () => {
mockUseApprovalRequest.mockReturnValue({
approvalRequest: mockApprovalRequest,
setApprovalRequest: jest.fn(),
isLoading: false,
errorFetchingApprovalRequest: null,
});
render(
<ApprovalRequestPage
{...baseProps}
organizations={[{ id: 1, name: "Test Org" }]}
userId={1}
/>
);
expect(screen.getByText("Test Request")).toBeInTheDocument();
expect(screen.getByText("Please approve this request")).toBeInTheDocument();
});
it("shows the approver actions when the user is the assignee and the request is active", () => {
mockUseApprovalRequest.mockReturnValue({
approvalRequest: mockApprovalRequest,
setApprovalRequest: jest.fn(),
isLoading: false,
errorFetchingApprovalRequest: null,
});
render(<ApprovalRequestPage {...baseProps} userId={2} />);
expect(screen.getAllByText("Approve request").length).toBeGreaterThan(0);
expect(screen.getAllByText("Deny request").length).toBeGreaterThan(0);
});
it("does not show the approver actions when the user is not the assignee", () => {
mockUseApprovalRequest.mockReturnValue({
approvalRequest: mockApprovalRequest,
setApprovalRequest: jest.fn(),
isLoading: false,
errorFetchingApprovalRequest: null,
});
render(
<ApprovalRequestPage
{...baseProps}
userId={3} // Different from assignee_user.id
/>
);
expect(screen.queryAllByText("Approve request").length).toBe(0);
expect(screen.queryAllByText("Deny request").length).toBe(0);
});
it("throws an error when the request fails", () => {
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(jest.fn());
const error = new Error("Failed to fetch");
mockUseApprovalRequest.mockReturnValue({
approvalRequest: null,
setApprovalRequest: jest.fn(),
isLoading: false,
errorFetchingApprovalRequest: error,
});
expect(() =>
render(<ApprovalRequestPage {...baseProps} userId={1} />)
).toThrow("Failed to fetch");
consoleSpy.mockRestore();
});
it("renders Clarification comment section when clarification_flow_messages is present (effectively arturo `approvals_clarification_flow_end_users` enabled)", () => {
const approvalRequestWithClarification = {
...mockApprovalRequest,
clarification_flow_messages: [],
};
mockUseApprovalRequest.mockReturnValue({
approvalRequest: approvalRequestWithClarification,
setApprovalRequest: jest.fn(),
isLoading: false,
errorFetchingApprovalRequest: null,
});
render(<ApprovalRequestPage {...baseProps} userId={1} />);
expect(screen.getByText(/Comments/i)).toBeInTheDocument();
});
it("renders without Clarification comment section when clarification_flow_messages is absent", () => {
mockUseApprovalRequest.mockReturnValue({
approvalRequest: mockApprovalRequest,
setApprovalRequest: jest.fn(),
isLoading: false,
errorFetchingApprovalRequest: null,
});
render(<ApprovalRequestPage {...baseProps} userId={1} />);
expect(screen.queryByText(/clarification/i)).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,193 @@
import { memo, useEffect } from "react";
import styled from "styled-components";
import { MD, XXL } from "@zendeskgarden/react-typography";
import { Spinner } from "@zendeskgarden/react-loaders";
import ApprovalRequestDetails from "./components/approval-request/ApprovalRequestDetails";
import ApprovalTicketDetails from "./components/approval-request/ApprovalTicketDetails";
import ApproverActions from "./components/approval-request/ApproverActions";
import { useApprovalRequest } from "./hooks/useApprovalRequest";
import type { Organization } from "../ticket-fields/data-types/Organization";
import ApprovalRequestBreadcrumbs from "./components/approval-request/ApprovalRequestBreadcrumbs";
import ClarificationContainer from "./components/approval-request/clarification/ClarificationContainer";
import { useUserViewedApprovalStatus } from "./hooks/useUserViewedApprovalStatus";
const Container = styled.div`
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-areas:
"left right"
"approverActions right"
"clarification right";
grid-gap: ${(props) => props.theme.space.lg};
margin-top: ${(props) => props.theme.space.xl}; /* 40px */
margin-bottom: ${(props) => props.theme.space.lg}; /* 32px */
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
grid-template-columns: 1fr;
grid-template-areas:
"left"
"right"
"approverActions"
"clarification";
margin-bottom: ${(props) => props.theme.space.xl};
}
`;
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const LeftColumn = styled.div`
grid-area: left;
& > *:first-child {
margin-bottom: ${(props) => props.theme.space.base * 4}px; /* 16px */
}
& > *:not(:first-child) {
margin-bottom: ${(props) => props.theme.space.lg}; /* 32px */
}
& > *:last-child {
margin-bottom: 0;
}
`;
const RightColumn = styled.div`
grid-area: right;
`;
const ClarificationArea = styled.div`
grid-area: clarification;
`;
const ApproverActionsWrapper = styled.div`
grid-area: approverActions;
margin-top: ${(props) => props.theme.space.lg};
`;
export interface ApprovalRequestPageProps {
approvalWorkflowInstanceId: string;
approvalRequestId: string;
baseLocale: string;
helpCenterPath: string;
organizations: Array<Organization>;
userId: number;
userAvatarUrl: string;
userName: string;
}
function ApprovalRequestPage({
approvalWorkflowInstanceId,
approvalRequestId,
baseLocale,
helpCenterPath,
organizations,
userId,
userAvatarUrl,
userName,
}: ApprovalRequestPageProps) {
const {
approvalRequest,
setApprovalRequest,
errorFetchingApprovalRequest: error,
isLoading,
} = useApprovalRequest(approvalWorkflowInstanceId, approvalRequestId);
const { hasUserViewedBefore, markUserViewed } = useUserViewedApprovalStatus({
approvalRequestId: approvalRequest?.id,
currentUserId: userId,
});
useEffect(() => {
const handleBeforeUnload = () => {
markUserViewed();
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [markUserViewed]);
if (error) {
throw error;
}
if (isLoading || !approvalRequest) {
return (
<LoadingContainer>
<Spinner size="64" />
</LoadingContainer>
);
}
const showApproverActions =
userId === approvalRequest.assignee_user.id &&
approvalRequest.status === "active";
// The `clarification_flow_messages` field is only present when arturo `approvals_clarification_flow_end_users` is enabled
const hasClarificationEnabled =
approvalRequest?.clarification_flow_messages !== undefined;
return (
<>
<ApprovalRequestBreadcrumbs
helpCenterPath={helpCenterPath}
organizations={organizations}
/>
<Container>
<LeftColumn>
<XXL isBold>{approvalRequest.subject}</XXL>
<MD>{approvalRequest.message}</MD>
{approvalRequest.ticket_details && (
<ApprovalTicketDetails ticket={approvalRequest.ticket_details} />
)}
</LeftColumn>
<RightColumn>
{approvalRequest && (
<ApprovalRequestDetails
approvalRequest={approvalRequest}
baseLocale={baseLocale}
/>
)}
</RightColumn>
{showApproverActions && (
<ApproverActionsWrapper>
<ApproverActions
approvalWorkflowInstanceId={approvalWorkflowInstanceId}
approvalRequestId={approvalRequestId}
setApprovalRequest={setApprovalRequest}
assigneeUser={approvalRequest.assignee_user}
/>
</ApproverActionsWrapper>
)}
{hasClarificationEnabled && (
<ClarificationArea>
<ClarificationContainer
approvalRequestId={approvalRequest.id}
baseLocale={baseLocale}
clarificationFlowMessages={
approvalRequest.clarification_flow_messages!
}
createdByUserId={approvalRequest.created_by_user.id}
currentUserAvatarUrl={userAvatarUrl}
currentUserId={userId}
currentUserName={userName}
hasUserViewedBefore={hasUserViewedBefore}
status={approvalRequest.status}
/>
</ClarificationArea>
)}
</Container>
</>
);
}
export default memo(ApprovalRequestPage);

View File

@@ -0,0 +1,98 @@
import { render } from "../../../test/render";
import { screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import ApprovalRequestListFilters from "./ApprovalRequestListFilters";
jest.mock(
"@zendeskgarden/svg-icons/src/16/search-stroke.svg",
() => "svg-mock"
);
const mockSetApprovalRequestStatus = jest.fn();
const mockSetSearchTerm = jest.fn();
describe("ApprovalRequestListFilters", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("renders the status filter with all options", async () => {
const user = userEvent.setup();
render(
<ApprovalRequestListFilters
approvalRequestStatus="any"
setApprovalRequestStatus={mockSetApprovalRequestStatus}
setSearchTerm={mockSetSearchTerm}
/>
);
expect(screen.getByLabelText("Status")).toBeInTheDocument();
const combobox = screen.getByRole("combobox", {
name: /status/i,
});
await user.click(combobox);
expect(screen.getByRole("option", { name: "Any" })).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Decision pending" })
).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Approved" })
).toBeInTheDocument();
expect(screen.getByRole("option", { name: "Denied" })).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Withdrawn" })
).toBeInTheDocument();
});
it("calls setApprovalRequestStatus when the status filter changes", async () => {
const user = userEvent.setup();
render(
<ApprovalRequestListFilters
approvalRequestStatus="any"
setApprovalRequestStatus={mockSetApprovalRequestStatus}
setSearchTerm={mockSetSearchTerm}
/>
);
const combobox = screen.getByRole("combobox", {
name: /status/i,
});
await user.click(combobox);
await user.click(screen.getByRole("option", { name: "Approved" }));
expect(mockSetApprovalRequestStatus).toHaveBeenCalledWith("approved");
waitFor(() => {
expect(combobox).toHaveValue("Approved");
});
});
it("calls setSearchTerm when the search input changes", async () => {
const user = userEvent.setup();
render(
<ApprovalRequestListFilters
approvalRequestStatus="any"
setApprovalRequestStatus={mockSetApprovalRequestStatus}
setSearchTerm={mockSetSearchTerm}
/>
);
const searchInput = screen.getByPlaceholderText(
/search approval requests/i
);
await user.click(searchInput);
await user.type(searchInput, "test search");
await waitFor(() => {
expect(mockSetSearchTerm).toHaveBeenCalledTimes(1);
expect(mockSetSearchTerm).toHaveBeenLastCalledWith("test search");
});
});
});

View File

@@ -0,0 +1,157 @@
import {
memo,
useCallback,
type Dispatch,
type SetStateAction,
useMemo,
} from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import debounce from "lodash.debounce";
import SearchIcon from "@zendeskgarden/svg-icons/src/16/search-stroke.svg";
import { Field, Combobox, Option } from "@zendeskgarden/react-dropdowns";
import { MediaInput } from "@zendeskgarden/react-forms";
import type { IComboboxProps } from "@zendeskgarden/react-dropdowns";
import type { ApprovalRequestDropdownStatus } from "../../types";
import { APPROVAL_REQUEST_STATES } from "../../constants";
const FiltersContainer = styled.div`
display: flex;
gap: ${(props) => props.theme.space.base * 17}px; /* 68px */
align-items: flex-end;
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
flex-direction: column;
align-items: normal;
gap: ${(props) => props.theme.space.base * 4}px; /* 16px */
}
`;
const SearchField = styled(Field)`
flex: 3;
`;
const DropdownFilterField = styled(Field)`
flex: 1;
`;
interface ApprovalRequestListFiltersProps {
approvalRequestStatus: ApprovalRequestDropdownStatus;
setApprovalRequestStatus: Dispatch<
SetStateAction<ApprovalRequestDropdownStatus>
>;
setSearchTerm: Dispatch<SetStateAction<string>>;
}
function ApprovalRequestListFilters({
approvalRequestStatus,
setApprovalRequestStatus,
setSearchTerm,
}: ApprovalRequestListFiltersProps) {
const { t } = useTranslation();
const getStatusLabel = useCallback(
(status: ApprovalRequestDropdownStatus) => {
switch (status) {
case "any":
return t("approval-requests.list.status-dropdown.any", "Any");
case "active":
return t(
"approval-requests.status.decision-pending",
"Decision pending"
);
case "approved":
return t("approval-requests.status.approved", "Approved");
case "rejected":
return t("approval-requests.status.denied", "Denied");
case "withdrawn":
return t("approval-requests.status.withdrawn", "Withdrawn");
}
},
[t]
);
const handleChange = useCallback<NonNullable<IComboboxProps["onChange"]>>(
(changes) => {
if (!changes.selectionValue) {
return;
}
setApprovalRequestStatus(
changes.selectionValue as ApprovalRequestDropdownStatus
);
setSearchTerm(""); // Reset search term when changing status
},
[setApprovalRequestStatus, setSearchTerm]
);
const debouncedSetSearchTerm = useMemo(
() => debounce((value: string) => setSearchTerm(value), 300),
[setSearchTerm]
);
const handleSearch = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearchTerm(event.target.value);
},
[debouncedSetSearchTerm]
);
return (
<FiltersContainer>
<SearchField>
<SearchField.Label hidden>
{t(
"approval-requests.list.search-placeholder",
"Search approval requests"
)}
</SearchField.Label>
<MediaInput
start={<SearchIcon />}
placeholder={t(
"approval-requests.list.search-placeholder",
"Search approval requests"
)}
onChange={handleSearch}
/>
</SearchField>
<DropdownFilterField>
<DropdownFilterField.Label>
{t("approval-requests.list.status-dropdown.label_v2", "Status")}
</DropdownFilterField.Label>
<Combobox
isEditable={false}
onChange={handleChange}
selectionValue={approvalRequestStatus}
inputValue={getStatusLabel(approvalRequestStatus)}
>
<Option
value="any"
label={t("approval-requests.list.status-dropdown.any", "Any")}
/>
<Option
value={APPROVAL_REQUEST_STATES.ACTIVE}
label={t(
"approval-requests.status.decision-pending",
"Decision pending"
)}
/>
<Option
value={APPROVAL_REQUEST_STATES.APPROVED}
label={t("approval-requests.status.approved", "Approved")}
/>
<Option
value={APPROVAL_REQUEST_STATES.REJECTED}
label={t("approval-requests.status.denied", "Denied")}
/>
<Option
value={APPROVAL_REQUEST_STATES.WITHDRAWN}
label={t("approval-requests.status.withdrawn", "Withdrawn")}
/>
</Combobox>
</DropdownFilterField>
</FiltersContainer>
);
}
export default memo(ApprovalRequestListFilters);

View File

@@ -0,0 +1,111 @@
import { render } from "../../../test/render";
import { screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import ApprovalRequestListTable from "./ApprovalRequestListTable";
import type { SearchApprovalRequest } from "../../types";
const mockApprovalRequests: SearchApprovalRequest[] = [
{
id: 123,
subject: "Hardware request",
requester_name: "Jane Doe",
created_by_name: "John Doe",
created_at: "2024-02-20T10:00:00Z",
status: "active",
},
{
id: 456,
subject: "Software license",
requester_name: "Jane Smith",
created_by_name: "Bob Smith",
created_at: "2024-02-19T15:00:00Z",
status: "approved",
},
];
const mockOnSortChange = jest.fn();
describe("ApprovalRequestListTable", () => {
it("renders the table headers correctly", () => {
render(
<ApprovalRequestListTable
approvalRequests={[]}
helpCenterPath="/hc/en-us"
baseLocale="en-US"
sortDirection="desc"
onSortChange={mockOnSortChange}
/>
);
// validate table headers
expect(screen.getByText("Subject")).toBeInTheDocument();
expect(screen.getByText("Requester")).toBeInTheDocument();
expect(screen.getByText("Sent by")).toBeInTheDocument();
expect(screen.getByText("Sent on")).toBeInTheDocument();
expect(screen.getByText("Approval status")).toBeInTheDocument();
});
it("renders approval requests with the correct data", () => {
render(
<ApprovalRequestListTable
approvalRequests={mockApprovalRequests}
helpCenterPath="/hc/en-us"
baseLocale="en-US"
sortDirection="desc"
onSortChange={mockOnSortChange}
/>
);
expect(screen.getByText("Hardware request")).toBeInTheDocument();
expect(screen.getByText("Hardware request")).toHaveAttribute(
"href",
"/hc/en-us/approval_requests/123"
);
expect(screen.getByText("Jane Doe")).toBeInTheDocument();
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText(/Feb 20, 2024/)).toBeInTheDocument();
expect(screen.getByText("Decision pending")).toBeInTheDocument();
expect(screen.getByText("Software license")).toBeInTheDocument();
expect(screen.getByText("Software license")).toHaveAttribute(
"href",
"/hc/en-us/approval_requests/456"
);
expect(screen.getByText("Jane Smith")).toBeInTheDocument();
expect(screen.getByText("Bob Smith")).toBeInTheDocument();
expect(screen.getByText(/Feb 19, 2024/)).toBeInTheDocument();
expect(screen.getByText("Approved")).toBeInTheDocument();
});
it("renders the no approval requests text when the filtered approval requests are empty", () => {
render(
<ApprovalRequestListTable
approvalRequests={[]}
helpCenterPath="/hc/en-us"
baseLocale="en-US"
sortDirection={undefined}
onSortChange={mockOnSortChange}
/>
);
expect(screen.getByText("No approval requests found.")).toBeInTheDocument();
});
it("calls the onSortChange function if the Sent On sortable header cell is clicked", async () => {
const user = userEvent.setup();
render(
<ApprovalRequestListTable
approvalRequests={[]}
helpCenterPath="/hc/en-us"
baseLocale="en-US"
sortDirection={undefined}
onSortChange={mockOnSortChange}
/>
);
const sentOnHeader = screen.getByText("Sent on");
await user.click(sentOnHeader);
expect(mockOnSortChange).toHaveBeenCalledWith("asc");
});
});

View File

@@ -0,0 +1,121 @@
import { memo } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { Table } from "@zendeskgarden/react-tables";
import { Anchor } from "@zendeskgarden/react-buttons";
import { getColor } from "@zendeskgarden/react-theming";
import ApprovalStatusTag from "../approval-request/ApprovalStatusTag";
import { formatApprovalRequestDate } from "../../utils";
import type { SearchApprovalRequest } from "../../types";
import NoApprovalRequestsText from "./NoApprovalRequestsText";
const ApprovalRequestAnchor = styled(Anchor)`
&:visited {
color: ${({ theme }) => getColor({ theme, hue: "blue", shade: 600 })};
}
`;
const sortableCellProps = {
style: {
paddingTop: "22px",
paddingBottom: "22px",
},
isTruncated: true,
};
interface ApprovalRequestListTableProps {
approvalRequests: SearchApprovalRequest[];
helpCenterPath: string;
baseLocale: string;
sortDirection: "asc" | "desc" | undefined;
onSortChange: (direction: "asc" | "desc" | undefined) => void;
}
function ApprovalRequestListTable({
approvalRequests,
helpCenterPath,
baseLocale,
sortDirection,
onSortChange,
}: ApprovalRequestListTableProps) {
const { t } = useTranslation();
const handleSortClick = () => {
if (sortDirection === "asc") {
onSortChange("desc");
} else if (sortDirection === "desc") {
onSortChange(undefined);
} else {
onSortChange("asc");
}
};
return (
<Table size="large">
<Table.Head>
<Table.HeaderRow>
<Table.HeaderCell width="40%" isTruncated>
{t("approval-requests.list.table.subject", "Subject")}
</Table.HeaderCell>
<Table.HeaderCell isTruncated>
{t("approval-requests.list.table.requester", "Requester")}
</Table.HeaderCell>
<Table.HeaderCell isTruncated>
{t("approval-requests.list.table.sent-by", "Sent by")}
</Table.HeaderCell>
<Table.SortableCell
onClick={handleSortClick}
sort={sortDirection}
cellProps={sortableCellProps}
>
{t("approval-requests.list.table.sent-on", "Sent on")}
</Table.SortableCell>
<Table.HeaderCell isTruncated>
{t(
"approval-requests.list.table.approval-status",
"Approval status"
)}
</Table.HeaderCell>
</Table.HeaderRow>
</Table.Head>
<Table.Body>
{approvalRequests.length === 0 ? (
<Table.Row>
<Table.Cell colSpan={5}>
<NoApprovalRequestsText />
</Table.Cell>
</Table.Row>
) : (
approvalRequests.map((approvalRequest) => (
<Table.Row key={approvalRequest.id}>
<Table.Cell isTruncated>
<ApprovalRequestAnchor
href={`${helpCenterPath}/approval_requests/${approvalRequest.id}`}
>
{approvalRequest.subject}
</ApprovalRequestAnchor>
</Table.Cell>
<Table.Cell isTruncated>
{approvalRequest.requester_name}
</Table.Cell>
<Table.Cell isTruncated>
{approvalRequest.created_by_name}
</Table.Cell>
<Table.Cell isTruncated>
{formatApprovalRequestDate(
approvalRequest.created_at,
baseLocale
)}
</Table.Cell>
<Table.Cell isTruncated>
<ApprovalStatusTag status={approvalRequest.status} />
</Table.Cell>
</Table.Row>
))
)}
</Table.Body>
</Table>
);
}
export default memo(ApprovalRequestListTable);

View File

@@ -0,0 +1,21 @@
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { MD } from "@zendeskgarden/react-typography";
import { getColor } from "@zendeskgarden/react-theming";
import { memo } from "react";
const StyledMD = styled(MD)`
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 600 })};
`;
function NoApprovalRequestsText() {
const { t } = useTranslation();
return (
<StyledMD>
{t("approval-requests.list.no-requests", "No approval requests found.")}
</StyledMD>
);
}
export default memo(NoApprovalRequestsText);

View File

@@ -0,0 +1,39 @@
import { render } from "../../../test/render";
import { screen } from "@testing-library/react";
import ApprovalRequestBreadcrumbs from "./ApprovalRequestBreadcrumbs";
describe("ApprovalRequestBreadcrumbs", () => {
it("renders breadcrumbs with organization name when organization exists", () => {
render(
<ApprovalRequestBreadcrumbs
helpCenterPath="/hc/en-us"
organizations={[{ id: 1, name: "Test Organization" }]}
/>
);
const orgLink = screen.getByRole("link", { name: "Test Organization" });
const approvalRequestsLink = screen.getByRole("link", {
name: "Approval requests",
});
expect(orgLink).toBeInTheDocument();
expect(orgLink).toHaveAttribute("href", "/hc/en-us");
expect(approvalRequestsLink).toHaveAttribute(
"href",
"/hc/en-us/approval_requests"
);
});
it("renders breadcrumbs without organization name when no organizations exist", () => {
render(
<ApprovalRequestBreadcrumbs
helpCenterPath="/hc/en-us"
organizations={[]}
/>
);
expect(
screen.getByRole("link", { name: "Approval requests" })
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,55 @@
import { memo } from "react";
import { Breadcrumb } from "@zendeskgarden/react-breadcrumbs";
import { useTranslation } from "react-i18next";
import { Anchor } from "@zendeskgarden/react-buttons";
import type { Organization } from "../../../ticket-fields/data-types/Organization";
import styled from "styled-components";
import { getColor } from "@zendeskgarden/react-theming";
const StyledBreadcrumb = styled(Breadcrumb)`
margin-top: ${(props) => props.theme.space.lg}; /* 32px */
`;
const BreadcrumbAnchor = styled(Anchor)`
&:visited {
color: ${({ theme }) => getColor({ theme, hue: "blue", shade: 600 })};
}
`;
interface ApprovalRequestBreadcrumbsProps {
helpCenterPath: string;
organizations: Array<Organization>;
}
function ApprovalRequestBreadcrumbs({
organizations,
helpCenterPath,
}: ApprovalRequestBreadcrumbsProps) {
const { t } = useTranslation();
const defaultOrganizationName =
organizations.length > 0 ? organizations[0]?.name : null;
if (defaultOrganizationName) {
return (
<StyledBreadcrumb>
<BreadcrumbAnchor href={helpCenterPath}>
{defaultOrganizationName}
</BreadcrumbAnchor>
<BreadcrumbAnchor href={`${helpCenterPath}/approval_requests`}>
{t("approval-requests.list.header", "Approval requests")}
</BreadcrumbAnchor>
</StyledBreadcrumb>
);
}
return (
<StyledBreadcrumb>
<BreadcrumbAnchor href={`${helpCenterPath}/approval_requests`}>
{t("approval-requests.list.header", "Approval requests")}
</BreadcrumbAnchor>
</StyledBreadcrumb>
);
}
export default memo(ApprovalRequestBreadcrumbs);

View File

@@ -0,0 +1,177 @@
import { render } from "../../../test/render";
import { screen } from "@testing-library/react";
import type { ApprovalRequest } from "../../types";
import ApprovalRequestDetails from "./ApprovalRequestDetails";
const mockApprovalRequest: ApprovalRequest = {
id: "123",
subject: "Test approval request",
message: "Please review this request",
status: "active",
created_at: "2024-02-20T10:30:00Z",
created_by_user: {
id: 123,
name: "John Sender",
photo: {
content_url: null,
},
},
assignee_user: {
id: 456,
name: "Jane Approver",
photo: {
content_url: null,
},
},
decided_at: null,
decisions: [],
withdrawn_reason: null,
ticket_details: {
id: "789",
priority: "normal",
custom_fields: [],
requester: {
id: 789,
name: "Request Creator",
photo: {
content_url: null,
},
},
},
};
describe("ApprovalRequestDetails", () => {
it("renders the basic approval request details without the decision notes and decided date", () => {
render(
<ApprovalRequestDetails
approvalRequest={mockApprovalRequest}
baseLocale="en-US"
/>
);
expect(screen.getByText("Approval request details")).toBeInTheDocument();
expect(screen.getByText("John Sender")).toBeInTheDocument();
expect(screen.getByText("Jane Approver")).toBeInTheDocument();
expect(screen.getByText("Decision pending")).toBeInTheDocument();
expect(screen.queryByText("Reason")).not.toBeInTheDocument();
expect(screen.queryByText("Decided")).not.toBeInTheDocument();
});
it("renders the decision notes and decided date when present", () => {
const approvalRequestWithNotesAndDecision: ApprovalRequest = {
...mockApprovalRequest,
status: "approved",
decided_at: "2024-02-21T15:45:00Z",
decisions: [
{
decision_notes: "This looks good to me",
decided_at: "2024-02-21T15:45:00Z",
decided_by_user: {
id: 456,
name: "Jane Approver",
photo: {
content_url: null,
},
},
status: "approved",
},
],
};
render(
<ApprovalRequestDetails
approvalRequest={approvalRequestWithNotesAndDecision}
baseLocale="en-US"
/>
);
expect(screen.getByText("Reason")).toBeInTheDocument();
expect(screen.getByText(/this looks good to me/i)).toBeInTheDocument();
expect(screen.getByText("Decided")).toBeInTheDocument();
expect(screen.getByText(/this looks good to me/i)).toBeInTheDocument();
});
it("renders a withdrawn approval request with the withdrawal reason", () => {
const withdrawnRequest: ApprovalRequest = {
...mockApprovalRequest,
status: "withdrawn",
withdrawn_reason: "No longer needed",
decided_at: "2024-02-21T15:45:00Z",
};
render(
<ApprovalRequestDetails
approvalRequest={withdrawnRequest}
baseLocale="en-US"
/>
);
expect(screen.getByText("Withdrawn on")).toBeInTheDocument();
expect(screen.getByText("No longer needed")).toBeInTheDocument();
});
it("shows the previous decision when an approval request is withdrawn with prior approval", () => {
const withdrawnWithPriorApproval: ApprovalRequest = {
...mockApprovalRequest,
status: "withdrawn",
withdrawn_reason: "Changed my mind",
decided_at: "2024-02-21T15:45:00Z",
decisions: [
{
decision_notes: "Originally stamped",
decided_at: "2024-02-20T10:30:00Z",
decided_by_user: {
id: 456,
name: "Jane Approver",
photo: {
content_url: null,
},
},
status: "approved",
},
],
};
render(
<ApprovalRequestDetails
approvalRequest={withdrawnWithPriorApproval}
baseLocale="en-US"
/>
);
expect(screen.getByText("Previous decision")).toBeInTheDocument();
expect(screen.getByText(/approved/i)).toBeInTheDocument();
expect(screen.getByText(/"Originally stamped"/)).toBeInTheDocument();
});
it("does not show the previous decision for non-withdrawn requests", () => {
const approvedRequest: ApprovalRequest = {
...mockApprovalRequest,
status: "approved",
decided_at: "2024-02-21T15:45:00Z",
decisions: [
{
decision_notes: "Looks good",
decided_at: "2024-02-21T15:45:00Z",
decided_by_user: {
id: 456,
name: "Jane Approver",
photo: {
content_url: null,
},
},
status: "approved",
},
],
};
render(
<ApprovalRequestDetails
approvalRequest={approvedRequest}
baseLocale="en-US"
/>
);
expect(screen.queryByText("Previous decision")).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,197 @@
import { memo } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { MD } from "@zendeskgarden/react-typography";
import { getColor } from "@zendeskgarden/react-theming";
import { Grid } from "@zendeskgarden/react-grid";
import type { ApprovalRequest } from "../../types";
import ApprovalStatusTag from "./ApprovalStatusTag";
import { formatApprovalRequestDate } from "../../utils";
import { APPROVAL_REQUEST_STATES } from "../../constants";
import ApprovalRequestPreviousDecision from "./ApprovalRequestPreviousDecision";
const Container = styled(Grid)`
padding: ${(props) => props.theme.space.base * 6}px; /* 24px */
margin-left: 0;
background: ${({ theme }) =>
getColor({ theme, variable: "background.default" })};
border-radius: ${(props) => props.theme.borderRadii.md}; /* 4px */
max-width: 296px;
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
max-width: 100%;
}
`;
const ApprovalRequestHeader = styled(MD)`
margin-bottom: ${(props) => props.theme.space.base * 4}px; /* 16px */
`;
const WrappedText = styled(MD)`
white-space: normal;
overflow-wrap: break-word;
`;
const FieldLabel = styled(MD)`
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 600 })};
`;
const DetailRow = styled(Grid.Row)`
margin-bottom: ${(props) => props.theme.space.sm}; /* 12px */
&:last-child {
margin-bottom: 0;
}
@media (max-width: ${(props) => props.theme.breakpoints.sm}) {
flex-direction: column; /* stack columns vertically */
> div {
width: 100% !important; /* full width for each Col */
max-width: 100% !important;
flex: none !important;
margin-bottom: ${(props) => props.theme.space.xxs}; /* 4px */
}
> div:last-child {
margin-bottom: 0;
}
}
`;
interface ApprovalRequestDetailsProps {
approvalRequest: ApprovalRequest;
baseLocale: string;
}
function ApprovalRequestDetails({
approvalRequest,
baseLocale,
}: ApprovalRequestDetailsProps) {
const { t } = useTranslation();
const shouldShowApprovalRequestComment =
approvalRequest.status === APPROVAL_REQUEST_STATES.WITHDRAWN
? Boolean(approvalRequest.withdrawn_reason)
: approvalRequest.decisions.length > 0;
const shouldShowPreviousDecision =
approvalRequest.status === APPROVAL_REQUEST_STATES.WITHDRAWN &&
approvalRequest.decisions.length > 0;
return (
<Container>
<ApprovalRequestHeader isBold>
{t(
"approval-requests.request.approval-request-details.header",
"Approval request details"
)}
</ApprovalRequestHeader>
<DetailRow>
<Grid.Col size={4}>
<FieldLabel>
{t(
"approval-requests.request.approval-request-details.sent-by",
"Sent by"
)}
</FieldLabel>
</Grid.Col>
<Grid.Col size={8}>
<WrappedText>{approvalRequest.created_by_user.name}</WrappedText>
</Grid.Col>
</DetailRow>
<DetailRow>
<Grid.Col size={4}>
<FieldLabel>
{t(
"approval-requests.request.approval-request-details.sent-on",
"Sent on"
)}
</FieldLabel>
</Grid.Col>
<Grid.Col size={8}>
<MD>
{formatApprovalRequestDate(approvalRequest.created_at, baseLocale)}
</MD>
</Grid.Col>
</DetailRow>
<DetailRow>
<Grid.Col size={4}>
<FieldLabel>
{t(
"approval-requests.request.approval-request-details.approver",
"Approver"
)}
</FieldLabel>
</Grid.Col>
<Grid.Col size={8}>
<WrappedText>{approvalRequest.assignee_user.name}</WrappedText>
</Grid.Col>
</DetailRow>
<DetailRow>
<Grid.Col size={4}>
<FieldLabel>
{t(
"approval-requests.request.approval-request-details.status",
"Status"
)}
</FieldLabel>
</Grid.Col>
<Grid.Col size={8}>
<MD>
<ApprovalStatusTag status={approvalRequest.status} />
</MD>
</Grid.Col>
</DetailRow>
{shouldShowApprovalRequestComment && (
<DetailRow>
<Grid.Col size={4}>
<FieldLabel>
{t(
"approval-requests.request.approval-request-details.comment_v2",
"Reason"
)}
</FieldLabel>
</Grid.Col>
<Grid.Col size={8}>
<WrappedText>
{approvalRequest.status === APPROVAL_REQUEST_STATES.WITHDRAWN
? approvalRequest.withdrawn_reason
: approvalRequest.decisions[0]?.decision_notes ?? "-"}
</WrappedText>
</Grid.Col>
</DetailRow>
)}
{approvalRequest.decided_at && (
<DetailRow>
<Grid.Col size={4}>
<FieldLabel>
{t(
approvalRequest.status === APPROVAL_REQUEST_STATES.WITHDRAWN
? "approval-requests.request.approval-request-details.withdrawn-on"
: "approval-requests.request.approval-request-details.decided",
approvalRequest.status === APPROVAL_REQUEST_STATES.WITHDRAWN
? "Withdrawn on"
: "Decided"
)}
</FieldLabel>
</Grid.Col>
<Grid.Col size={8}>
<MD>
{formatApprovalRequestDate(
approvalRequest.decided_at,
baseLocale
)}
</MD>
</Grid.Col>
</DetailRow>
)}
{shouldShowPreviousDecision && approvalRequest.decisions[0] && (
<ApprovalRequestPreviousDecision
decision={approvalRequest.decisions[0]}
baseLocale={baseLocale}
/>
)}
</Container>
);
}
export default memo(ApprovalRequestDetails);

View File

@@ -0,0 +1,49 @@
import { render } from "../../../test/render";
import { screen } from "@testing-library/react";
import ApprovalRequestPreviousDecision from "./ApprovalRequestPreviousDecision";
import type { ApprovalDecision } from "../../types";
const mockDecision: ApprovalDecision = {
decision_notes: "Looks good to me",
decided_at: "2024-02-21T15:45:00Z",
decided_by_user: {
id: 456,
name: "Jane Approver",
photo: {
content_url: null,
},
},
status: "approved",
};
describe("ApprovalRequestPreviousDecision", () => {
it("renders the previous decision header, status, and notes", () => {
render(
<ApprovalRequestPreviousDecision
decision={mockDecision}
baseLocale="en-US"
/>
);
expect(screen.getByText("Previous decision")).toBeInTheDocument();
expect(screen.getByText(/approved/i)).toBeInTheDocument();
expect(screen.getByText(/"Looks good to me"/)).toBeInTheDocument();
});
it("renders the decision without decision notes if they do not exist", () => {
const decisionWithoutNotes: ApprovalDecision = {
...mockDecision,
decision_notes: null,
};
render(
<ApprovalRequestPreviousDecision
decision={decisionWithoutNotes}
baseLocale="en-US"
/>
);
expect(screen.getByText(/approved/i)).toBeInTheDocument();
expect(screen.queryByText(/"/)).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,71 @@
import { memo } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { MD } from "@zendeskgarden/react-typography";
import { getColor } from "@zendeskgarden/react-theming";
import { APPROVAL_REQUEST_STATES } from "../../constants";
import { formatApprovalRequestDate } from "../../utils";
import type { ApprovalDecision } from "../../types";
const Container = styled.div`
border-top: ${({ theme }) =>
`1px solid ${getColor({ theme, hue: "grey", shade: 300 })}`};
display: flex;
flex-direction: column;
padding-top: ${(props) => props.theme.space.base * 4}px; /* 16px */
`;
const PreviousDecisionTitle = styled(MD)`
margin-bottom: ${(props) => props.theme.space.xxs}; /* 4px */
`;
const FieldLabel = styled(MD)`
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 600 })};
`;
function getPreviousDecisionFallbackLabel(status: string) {
switch (status) {
case APPROVAL_REQUEST_STATES.APPROVED:
return "Approved";
case APPROVAL_REQUEST_STATES.REJECTED:
return "Rejected";
default:
return status;
}
}
interface ApprovalRequestPreviousDecisionProps {
decision: ApprovalDecision;
baseLocale: string;
}
function ApprovalRequestPreviousDecision({
decision,
baseLocale,
}: ApprovalRequestPreviousDecisionProps) {
const { t } = useTranslation();
return (
<Container>
<PreviousDecisionTitle>
{t(
"approval-requests.request.approval-request-details.previous-decision",
"Previous decision"
)}
</PreviousDecisionTitle>
<FieldLabel>
{t(
`approval-requests.request.approval-request-details.${decision.status.toLowerCase()}`,
getPreviousDecisionFallbackLabel(decision.status)
)}{" "}
{formatApprovalRequestDate(decision.decided_at ?? "", baseLocale)}
</FieldLabel>
{decision.decision_notes && (
// eslint-disable-next-line @shopify/jsx-no-hardcoded-content
<FieldLabel>{`"${decision.decision_notes}"`}</FieldLabel>
)}
</Container>
);
}
export default memo(ApprovalRequestPreviousDecision);

View File

@@ -0,0 +1,29 @@
import { render } from "../../../test/render";
import { screen } from "@testing-library/react";
import ApprovalStatusTag from "./ApprovalStatusTag";
describe("ApprovalStatusTag", () => {
it("renders the active status tag correctly", () => {
render(<ApprovalStatusTag status="active" />);
expect(screen.getByText("Decision pending")).toBeInTheDocument();
});
it("renders the approved status tag correctly", () => {
render(<ApprovalStatusTag status="approved" />);
expect(screen.getByText("Approved")).toBeInTheDocument();
});
it("renders the rejected status tag correctly", () => {
render(<ApprovalStatusTag status="rejected" />);
expect(screen.getByText("Denied")).toBeInTheDocument();
});
it("renders the withdrawn status tag correctly", () => {
render(<ApprovalStatusTag status="withdrawn" />);
expect(screen.getByText("Withdrawn")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,43 @@
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { Tag } from "@zendeskgarden/react-tags";
import { Ellipsis } from "@zendeskgarden/react-typography";
import type { ApprovalRequestStatus } from "../../types";
import { APPROVAL_REQUEST_STATES } from "../../constants";
const DEFAULT_STATUS_CONFIG = { hue: "grey", label: "Unknown status" };
interface ApprovalStatusTagProps {
status: ApprovalRequestStatus;
}
function ApprovalStatusTag({ status }: ApprovalStatusTagProps) {
const { t } = useTranslation();
const statusTagMap = {
[APPROVAL_REQUEST_STATES.ACTIVE]: {
hue: "blue",
label: t("approval-requests.status.decision-pending", "Decision pending"),
},
[APPROVAL_REQUEST_STATES.APPROVED]: {
hue: "green",
label: t("approval-requests.status.approved", "Approved"),
},
[APPROVAL_REQUEST_STATES.REJECTED]: {
hue: "red",
label: t("approval-requests.status.denied", "Denied"),
},
[APPROVAL_REQUEST_STATES.WITHDRAWN]: {
hue: "grey",
label: t("approval-requests.status.withdrawn", "Withdrawn"),
},
};
const config = statusTagMap[status] || DEFAULT_STATUS_CONFIG;
return (
<Tag hue={config.hue}>
<Ellipsis>{config.label}</Ellipsis>
</Tag>
);
}
export default memo(ApprovalStatusTag);

View File

@@ -0,0 +1,96 @@
import { render } from "../../../test/render";
import { screen } from "@testing-library/react";
import ApprovalTicketDetails from "./ApprovalTicketDetails";
import type { ApprovalRequestTicket } from "../../types";
const mockTicket: ApprovalRequestTicket = {
id: "123",
priority: "normal",
requester: {
id: 456,
name: "John Requester",
photo: {
content_url: null,
},
},
custom_fields: [],
};
describe("ApprovalTicketDetails", () => {
it("renders basic ticket details", () => {
render(<ApprovalTicketDetails ticket={mockTicket} />);
expect(screen.getByText("Ticket details")).toBeInTheDocument();
expect(screen.getByText("John Requester")).toBeInTheDocument();
expect(screen.getByText("123")).toBeInTheDocument();
expect(screen.getByText("normal")).toBeInTheDocument();
});
it("renders custom fields with string values", () => {
const ticketWithCustomFields: ApprovalRequestTicket = {
...mockTicket,
custom_fields: [
{ id: "field1", title_in_portal: "Department", value: "IT" },
{ id: "field2", title_in_portal: "Cost Center", value: "123-45" },
],
};
render(<ApprovalTicketDetails ticket={ticketWithCustomFields} />);
expect(screen.getByText("Department")).toBeInTheDocument();
expect(screen.getByText("IT")).toBeInTheDocument();
expect(screen.getByText("Cost Center")).toBeInTheDocument();
expect(screen.getByText("123-45")).toBeInTheDocument();
});
it("renders custom fields with boolean values", () => {
const ticketWithBooleanFields: ApprovalRequestTicket = {
...mockTicket,
custom_fields: [
{ id: "field1", title_in_portal: "Urgent", value: true },
{ id: "field2", title_in_portal: "Reviewed", value: false },
],
};
render(<ApprovalTicketDetails ticket={ticketWithBooleanFields} />);
expect(screen.getByText("Urgent")).toBeInTheDocument();
expect(screen.getByText("Yes")).toBeInTheDocument();
expect(screen.getByText("Reviewed")).toBeInTheDocument();
expect(screen.getByText("No")).toBeInTheDocument();
});
it("renders custom fields with array values", () => {
const ticketWithArrayFields: ApprovalRequestTicket = {
...mockTicket,
custom_fields: [
{
id: "field1",
title_in_portal: "Categories",
value: ["Hardware", "Software"],
},
],
};
render(<ApprovalTicketDetails ticket={ticketWithArrayFields} />);
expect(screen.getByText("Categories")).toBeInTheDocument();
expect(screen.getByText("Hardware")).toBeInTheDocument();
expect(screen.getByText("Software")).toBeInTheDocument();
});
it("renders placeholder for empty or undefined custom field values", () => {
const ticketWithEmptyFields: ApprovalRequestTicket = {
...mockTicket,
custom_fields: [
{ id: "field1", title_in_portal: "Empty Field", value: undefined },
{ id: "field2", title_in_portal: "Empty Array", value: [] },
],
};
render(<ApprovalTicketDetails ticket={ticketWithEmptyFields} />);
expect(screen.getByText("Empty Field")).toBeInTheDocument();
expect(screen.getAllByText("-")).toHaveLength(2);
});
});

View File

@@ -0,0 +1,145 @@
import { memo } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { MD } from "@zendeskgarden/react-typography";
import { getColor } from "@zendeskgarden/react-theming";
import { Grid } from "@zendeskgarden/react-grid";
import { Tag } from "@zendeskgarden/react-tags";
import type { ApprovalRequestTicket } from "../../types";
const TicketContainer = styled(Grid)`
padding: ${(props) => props.theme.space.md}; /* 20px */
border: ${(props) => props.theme.borders.sm}
${({ theme }) => getColor({ theme, hue: "grey", shade: 300 })};
border-radius: ${(props) => props.theme.borderRadii.md}; /* 4px */
`;
const TicketDetailsHeader = styled(MD)`
margin-bottom: ${(props) => props.theme.space.md}; /* 20px */
`;
const FieldLabel = styled(MD)`
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 600 })};
`;
const MultiselectTag = styled(Tag)`
margin-inline-end: ${(props) => props.theme.space.xxs}; /* 4px */
`;
const CustomFieldsGrid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: ${(props) => props.theme.space.md}; /* 20px */
margin-top: ${(props) => props.theme.space.md}; /* 20px */
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
grid-template-columns: repeat(2, 1fr);
}
`;
const NULL_VALUE_PLACEHOLDER = "-";
function CustomFieldValue({
value,
}: {
value: string | boolean | Array<string> | undefined;
}) {
const { t } = useTranslation();
if (Array.isArray(value) && value.length > 0) {
return (
<MD>
{value.map((val) => (
<MultiselectTag key={val} hue="grey">
{val}
</MultiselectTag>
))}
</MD>
);
}
if (typeof value === "boolean") {
return (
<MD>
{value
? t(
"approval-requests.request.ticket-details.checkbox-value.yes",
"Yes"
)
: t(
"approval-requests.request.ticket-details.checkbox-value.no",
"No"
)}
</MD>
);
}
if (!value || (Array.isArray(value) && value.length === 0)) {
return <MD>{NULL_VALUE_PLACEHOLDER}</MD>;
}
return <MD>{value}</MD>;
}
interface ApprovalTicketDetailsProps {
ticket: ApprovalRequestTicket;
}
const TicketPriorityKeys = {
Low: "approval-requests.request.ticket-details.priority_low",
Normal: "approval-requests.request.ticket-details.priority_normal",
High: "approval-requests.request.ticket-details.priority_high",
Urgent: "approval-requests.request.ticket-details.priority_urgent",
};
function ApprovalTicketDetails({ ticket }: ApprovalTicketDetailsProps) {
const { t } = useTranslation();
const displayPriority = TicketPriorityKeys[
ticket.priority as keyof typeof TicketPriorityKeys
]
? t(
TicketPriorityKeys[ticket.priority as keyof typeof TicketPriorityKeys],
ticket.priority
)
: ticket.priority;
return (
<TicketContainer>
<TicketDetailsHeader isBold>
{t("approval-requests.request.ticket-details.header", "Ticket details")}
</TicketDetailsHeader>
<CustomFieldsGrid>
<div>
<FieldLabel>
{t(
"approval-requests.request.ticket-details.requester",
"Requester"
)}
</FieldLabel>
<MD>{ticket.requester.name}</MD>
</div>
<div>
<FieldLabel>
{t("approval-requests.request.ticket-details.id", "ID")}
</FieldLabel>
<MD>{ticket.id}</MD>
</div>
<div>
<FieldLabel>
{t("approval-requests.request.ticket-details.priority", "Priority")}
</FieldLabel>
<MD>{displayPriority}</MD>
</div>
{ticket.custom_fields.map((field) => (
<div key={String(field.id)}>
<FieldLabel>{field.title_in_portal}</FieldLabel>
<CustomFieldValue value={field.value} />
</div>
))}
</CustomFieldsGrid>
</TicketContainer>
);
}
export default memo(ApprovalTicketDetails);

View File

@@ -0,0 +1,274 @@
import { render } from "../../../test/render";
import { screen, waitFor, act } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import ApproverActions from "./ApproverActions";
import { notify } from "../../../shared";
jest.mock("../../../shared/notifications", () => ({
notify: jest.fn(),
}));
const mockNotify = notify as jest.Mock;
const mockSubmitApprovalDecision = jest.fn();
jest.mock("../../submitApprovalDecision", () => ({
submitApprovalDecision: (...args: unknown[]) =>
mockSubmitApprovalDecision(...args),
}));
const mockAssigneeUser = {
id: 123,
name: "Test User",
photo: { content_url: null },
};
const mockApprovalRequestId = "123";
const mockApprovalWorkflowInstanceId = "456";
const mockSetApprovalRequest = jest.fn();
describe("ApproverActions", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("initially renders the approve and deny buttons", () => {
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
expect(screen.getByText("Approve request")).toBeInTheDocument();
expect(screen.getByText("Deny request")).toBeInTheDocument();
});
it("shows the comment section when clicking Approve request", async () => {
const user = userEvent.setup();
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Approve request"));
expect(screen.getByText("Additional note")).toBeInTheDocument();
expect(screen.getByText("Submit approval")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
});
it("does not show the Avatar when assigneeUser has no photo", async () => {
const user = userEvent.setup();
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Approve request"));
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
it("shows the Avatar when assigneeUser has a photo", async () => {
const assigneeUserWithPhoto = {
id: 123,
name: "Test User",
photo: {
content_url: "https://example.com/avatar.jpg",
},
};
const user = userEvent.setup();
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={assigneeUserWithPhoto}
/>
);
await user.click(screen.getByText("Approve request"));
const avatar = screen.getByRole("img");
expect(avatar).toBeInTheDocument();
expect(avatar).toHaveAttribute("src", "https://example.com/avatar.jpg");
expect(avatar).toHaveAttribute("alt", "Assignee avatar");
});
it("shows the comment section with required field when clicking Deny request", async () => {
const user = userEvent.setup();
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Deny request"));
expect(
screen.getByText("Reason for denial* (Required)")
).toBeInTheDocument();
expect(screen.getByText("Submit denial")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
});
it("shows the validation error when submitting an empty denial", async () => {
const user = userEvent.setup();
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Deny request"));
await user.click(screen.getByText("Submit denial"));
expect(screen.getByText("Enter a reason for denial")).toBeInTheDocument();
expect(mockSubmitApprovalDecision).not.toHaveBeenCalled();
});
it("returns to initial state when clicking Cancel", async () => {
const user = userEvent.setup();
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Approve request"));
await user.click(screen.getByText("Cancel"));
expect(screen.getByText("Approve request")).toBeInTheDocument();
expect(screen.getByText("Deny request")).toBeInTheDocument();
});
it("handles successful approval submission", async () => {
const user = userEvent.setup();
mockSubmitApprovalDecision.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ approval_request: { id: "123" } }),
});
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Approve request"));
await user.type(screen.getByRole("textbox"), "Test comment");
await act(async () => {
await user.click(screen.getByText("Submit approval"));
});
await waitFor(() => {
expect(mockSubmitApprovalDecision).toHaveBeenCalledWith(
"456",
"123",
"approved",
"Test comment"
);
});
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: "success",
title: "Approval submitted",
message: "",
});
});
});
it("handles successful denial submission", async () => {
const user = userEvent.setup();
mockSubmitApprovalDecision.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ approval_request: { id: "123" } }),
});
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Deny request"));
await user.type(screen.getByRole("textbox"), "Denial reason");
await act(async () => {
await user.click(screen.getByText("Submit denial"));
});
await waitFor(() => {
expect(mockSubmitApprovalDecision).toHaveBeenCalledWith(
"456",
"123",
"rejected",
"Denial reason"
);
});
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: "success",
title: "Denial submitted",
message: "",
});
});
});
it("handles failed submission", async () => {
const user = userEvent.setup();
mockSubmitApprovalDecision.mockResolvedValue({ ok: false });
render(
<ApproverActions
approvalRequestId={mockApprovalRequestId}
approvalWorkflowInstanceId={mockApprovalWorkflowInstanceId}
setApprovalRequest={mockSetApprovalRequest}
assigneeUser={mockAssigneeUser}
/>
);
await user.click(screen.getByText("Approve request"));
await act(async () => {
await user.click(screen.getByText("Submit approval"));
});
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: "error",
title: "Error submitting decision",
message: "Please try again later",
});
});
});
});

View File

@@ -0,0 +1,244 @@
import { useState, useCallback, memo } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { Button } from "@zendeskgarden/react-buttons";
import { Field, Textarea } from "@zendeskgarden/react-forms";
import { Avatar } from "@zendeskgarden/react-avatars";
import { submitApprovalDecision } from "../../submitApprovalDecision";
import type { ApprovalDecision } from "../../submitApprovalDecision";
import type { ApprovalRequest } from "../../types";
import { APPROVAL_REQUEST_STATES } from "../../constants";
import { notify } from "../../../shared";
const PENDING_APPROVAL_STATUS = {
APPROVED: "APPROVED",
REJECTED: "REJECTED",
} as const;
const ButtonContainer = styled.div<{
hasAvatar?: boolean;
isSubmitButton?: boolean;
}>`
display: flex;
flex-direction: row;
gap: ${(props) => props.theme.space.md}; /* 20px */
margin-inline-start: ${(props) =>
props.hasAvatar ? "55px" : "0"}; // avatar width + margin + border
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
flex-direction: ${(props) =>
props.isSubmitButton ? "row-reverse" : "column"};
gap: ${(props) => props.theme.space.base * 4}px; /* 16px */
}
`;
const CommentSection = styled.div`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.space.lg}; /* 32px */
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
gap: ${(props) => props.theme.space.base * 4}px; /* 16px */
}
`;
const TextAreaContainer = styled.div`
display: flex;
gap: ${(props) => props.theme.space.base * 4}px; /* 16px */
margin-top: ${(props) => props.theme.space.base * 6}px; /* 24px */
align-items: flex-start;
`;
const TextAreaAndMessage = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`;
interface ApproverActionsProps {
approvalRequestId: string;
approvalWorkflowInstanceId: string;
setApprovalRequest: (approvalRequest: ApprovalRequest) => void;
assigneeUser: ApprovalRequest["assignee_user"];
}
function ApproverActions({
approvalRequestId,
approvalWorkflowInstanceId,
setApprovalRequest,
assigneeUser,
}: ApproverActionsProps) {
const { t } = useTranslation();
const [comment, setComment] = useState("");
const [pendingStatus, setPendingStatus] = useState<
| (typeof PENDING_APPROVAL_STATUS)[keyof typeof PENDING_APPROVAL_STATUS]
| null
>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showValidation, setShowValidation] = useState(false);
const isCommentValid =
pendingStatus !== PENDING_APPROVAL_STATUS.REJECTED || comment.trim() !== "";
const shouldShowValidationError = showValidation && !isCommentValid;
const handleApproveRequestClick = useCallback(() => {
setPendingStatus(PENDING_APPROVAL_STATUS.APPROVED);
setShowValidation(false);
}, []);
const handleDenyRequestClick = useCallback(() => {
setPendingStatus(PENDING_APPROVAL_STATUS.REJECTED);
setShowValidation(false);
}, []);
const handleInputValueChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComment(e.target.value);
},
[]
);
const handleCancelClick = useCallback(() => {
setPendingStatus(null);
setComment("");
setShowValidation(false);
}, []);
const handleSubmitDecisionClick = async () => {
setShowValidation(true);
if (!pendingStatus || !isCommentValid) return;
setIsSubmitting(true);
try {
const decision: ApprovalDecision =
pendingStatus === PENDING_APPROVAL_STATUS.APPROVED
? APPROVAL_REQUEST_STATES.APPROVED
: APPROVAL_REQUEST_STATES.REJECTED;
const response = await submitApprovalDecision(
approvalWorkflowInstanceId,
approvalRequestId,
decision,
comment
);
if (response.ok) {
const data = await response.json();
setApprovalRequest(data.approval_request);
const notificationTitle =
decision === APPROVAL_REQUEST_STATES.APPROVED
? t(
"approval-requests.request.notification.approval-submitted",
"Approval submitted"
)
: t(
"approval-requests.request.notification.denial-submitted",
"Denial submitted"
);
notify({
type: "success",
title: notificationTitle,
message: "",
});
} else {
throw new Error(`Failed to submit ${decision} decision`);
}
} catch (error) {
notify({
type: "error",
title: "Error submitting decision",
message: "Please try again later",
});
} finally {
setIsSubmitting(false);
}
};
if (pendingStatus) {
const fieldLabel =
pendingStatus === PENDING_APPROVAL_STATUS.APPROVED
? t(
"approval-requests.request.approver-actions.additional-note-label",
"Additional note"
)
: t(
"approval-requests.request.approver-actions.denial-reason-label",
"Reason for denial* (Required)"
);
const shouldShowAvatar = Boolean(assigneeUser?.photo?.content_url);
return (
<CommentSection>
<Field>
<Field.Label>{fieldLabel}</Field.Label>
<TextAreaContainer>
{shouldShowAvatar && (
<Avatar>
<img
alt="Assignee avatar"
src={assigneeUser.photo.content_url ?? undefined}
/>
</Avatar>
)}
<TextAreaAndMessage>
<Textarea
minRows={5}
value={comment}
onChange={handleInputValueChange}
disabled={isSubmitting}
validation={shouldShowValidationError ? "error" : undefined}
/>
{shouldShowValidationError && (
<Field.Message validation="error">
{t(
"approval-requests.request.approver-actions.denial-reason-validation",
"Enter a reason for denial"
)}
</Field.Message>
)}
</TextAreaAndMessage>
</TextAreaContainer>
</Field>
<ButtonContainer hasAvatar={shouldShowAvatar} isSubmitButton>
<Button
isPrimary
onClick={handleSubmitDecisionClick}
disabled={isSubmitting}
>
{pendingStatus === PENDING_APPROVAL_STATUS.APPROVED
? t(
"approval-requests.request.approver-actions.submit-approval",
"Submit approval"
)
: t(
"approval-requests.request.approver-actions.submit-denial",
"Submit denial"
)}
</Button>
<Button onClick={handleCancelClick} disabled={isSubmitting}>
{t("approval-requests.request.approver-actions.cancel", "Cancel")}
</Button>
</ButtonContainer>
</CommentSection>
);
}
return (
<ButtonContainer>
<Button isPrimary onClick={handleApproveRequestClick}>
{t(
"approval-requests.request.approver-actions.approve-request",
"Approve request"
)}
</Button>
<Button onClick={handleDenyRequestClick}>
{t(
"approval-requests.request.approver-actions.deny-request",
"Deny request"
)}
</Button>
</ButtonContainer>
);
}
export default memo(ApproverActions);

View File

@@ -0,0 +1,57 @@
import type React from "react";
import styled from "styled-components";
import { Avatar } from "@zendeskgarden/react-avatars";
import { getColor } from "@zendeskgarden/react-theming";
import HeadsetBadge from "@zendeskgarden/svg-icons/src/16/headset-fill.svg";
import { DEFAULT_AVATAR_URL } from "./constants";
const AvatarWrapper = styled.div`
position: relative;
display: inline-block;
`;
const HeadsetBadgeWrapper = styled.div`
position: absolute;
bottom: -3px;
right: -3px;
border-radius: 50%;
width: 13px;
height: 13px;
display: flex;
align-items: center;
justify-content: center;
background-color: ${({ theme }) =>
getColor({ theme, hue: "grey", shade: 100 })};
border: ${({ theme }) => theme.borders.sm};
`;
const HeadsetIcon = styled(HeadsetBadge)`
width: 9px;
height: 9px;
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 900 })};
`;
interface AvatarWithBadgeProps {
name: string;
photoUrl?: string;
size: "small" | "medium" | "large";
}
const AvatarWithBadge: React.FC<AvatarWithBadgeProps> = ({
name,
photoUrl,
size,
}) => {
return (
<AvatarWrapper>
<Avatar size={size}>
<img alt={name} src={photoUrl ? photoUrl : DEFAULT_AVATAR_URL} />
</Avatar>
<HeadsetBadgeWrapper>
<HeadsetIcon />
</HeadsetBadgeWrapper>
</AvatarWrapper>
);
};
export default AvatarWithBadge;

View File

@@ -0,0 +1,106 @@
import type React from "react";
import { useRef, memo } from "react";
import { Grid, Col, Row } from "@zendeskgarden/react-grid";
import { Avatar } from "@zendeskgarden/react-avatars";
import styled from "styled-components";
import { type ApprovalClarificationFlowMessage } from "../../../types";
import { useIntersectionObserver } from "./hooks/useIntersectionObserver";
import { RelativeTime } from "./RelativeTime";
import AvatarWithHeadsetBadge from "./AvatarWithBadge";
import { DEFAULT_AVATAR_URL } from "./constants";
import Circle from "@zendeskgarden/svg-icons/src/12/circle-sm-fill.svg";
import { getColor } from "@zendeskgarden/react-theming";
import { MD } from "@zendeskgarden/react-typography";
const MessageContainer = styled.div`
margin-top: ${({ theme }) => theme.space.sm};
`;
const Body = styled.div`
margin-top: ${({ theme }) => theme.space.xs};
`;
const AvatarCol = styled(Col)`
max-width: 55px;
`;
const CircleWrapper = styled.span`
padding: 0px 6px;
`;
const NameAndTimestampCol = styled(Col)`
display: flex;
flex-direction: row;
align-items: start;
`;
const StyledCircle = styled(Circle)`
width: ${({ theme }) => theme.space.xs};
height: ${({ theme }) => theme.space.xs};
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 600 })};
`;
export interface ClarificationCommentProps {
baseLocale: string;
children?: React.ReactNode;
comment: ApprovalClarificationFlowMessage;
commentKey: string;
createdByUserId: number;
markCommentAsVisible: (commentKey: string) => void;
}
function ClarificationCommentComponent({
baseLocale,
children,
comment,
commentKey,
createdByUserId,
markCommentAsVisible,
}: ClarificationCommentProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const { author, created_at } = comment;
const { avatar, name, id: authorId } = author;
useIntersectionObserver(containerRef, () => markCommentAsVisible(commentKey));
const isRequester = createdByUserId === authorId;
return (
<MessageContainer ref={containerRef}>
<Grid gutters={false}>
<Row>
<AvatarCol>
{isRequester ? (
<AvatarWithHeadsetBadge
photoUrl={avatar}
size="small"
name={name}
/>
) : (
<Avatar size="small">
<img alt={name} src={avatar ? avatar : DEFAULT_AVATAR_URL} />
</Avatar>
)}
</AvatarCol>
<Col>
<Row alignItems="start" justifyContent="start">
<NameAndTimestampCol>
<MD isBold>{name}</MD>
<CircleWrapper>
<StyledCircle />
</CircleWrapper>
{created_at && (
<RelativeTime eventTime={created_at} locale={baseLocale} />
)}
</NameAndTimestampCol>
</Row>
<Body>{children}</Body>
</Col>
</Row>
</Grid>
</MessageContainer>
);
}
const ClarificationComment = memo(ClarificationCommentComponent);
export default ClarificationComment;

View File

@@ -0,0 +1,123 @@
import { Grid, Col, Row } from "@zendeskgarden/react-grid";
import { Field, Textarea, Label, Message } from "@zendeskgarden/react-forms";
import { useGetClarificationCopy } from "./hooks/useGetClarificationCopy";
import { Avatar } from "@zendeskgarden/react-avatars";
import { useCommentForm } from "./hooks/useCommentForm";
import { useSubmitComment } from "./hooks/useSubmitComment";
import { Button } from "@zendeskgarden/react-buttons";
import styled from "styled-components";
import { DEFAULT_AVATAR_URL } from "./constants";
interface ClarificationCommentFormProps {
baseLocale: string;
currentUserAvatarUrl: string;
currentUserName: string;
markAllCommentsAsRead: () => void;
}
const FormContainer = styled(Grid)`
margin-top: ${({ theme }) => theme.space.xs};
padding-top: ${({ theme }) => theme.space.md};
`;
const AvatarCol = styled(Col)`
max-width: 55px;
`;
const ButtonsRow = styled(Row)`
margin-top: 10px;
margin-left: 55px;
`;
const CancelButton = styled(Button)`
margin: 10px;
`;
function ClarificationCommentForm({
baseLocale,
currentUserAvatarUrl,
currentUserName,
markAllCommentsAsRead,
}: ClarificationCommentFormProps) {
const {
comment_form_aria_label,
submit_button,
cancel_button,
validation_empty_input,
} = useGetClarificationCopy();
const { handleSubmitComment, isLoading = false } = useSubmitComment();
const {
buttonsContainerRef,
comment,
commentValidation,
charLimitMessage,
handleBlur,
handleCancel,
handleFocus,
handleKeyDown,
handleSubmit,
handleChange,
isInputFocused,
textareaRef,
} = useCommentForm({
onSubmit: handleSubmitComment,
baseLocale,
markAllCommentsAsRead,
});
return (
<FormContainer gutters={false}>
<Row>
<AvatarCol>
<Avatar size="small">
<img
alt={currentUserName}
src={
currentUserAvatarUrl ? currentUserAvatarUrl : DEFAULT_AVATAR_URL
}
/>
</Avatar>
</AvatarCol>
<Col>
<Field>
<Label hidden>{comment_form_aria_label}</Label>
<Textarea
ref={textareaRef}
validation={commentValidation}
minRows={isInputFocused || comment.trim().length > 0 ? 4 : 1}
maxRows={4}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onFocus={handleFocus}
/>
<Message validation={commentValidation}>
{commentValidation === "error"
? validation_empty_input
: commentValidation === "warning"
? charLimitMessage
: null}
</Message>
</Field>
</Col>
</Row>
{(isInputFocused || comment.trim().length > 0) && (
<ButtonsRow ref={buttonsContainerRef}>
<Col textAlign="start">
<Button disabled={isLoading} onClick={handleSubmit}>
{submit_button}
</Button>
<CancelButton disabled={isLoading} onClick={handleCancel} isBasic>
{cancel_button}
</CancelButton>
</Col>
</ButtonsRow>
)}
</FormContainer>
);
}
export default ClarificationCommentForm;

View File

@@ -0,0 +1,162 @@
import React, { useEffect } from "react";
import { useGetClarificationCopy } from "./hooks/useGetClarificationCopy";
import { getColor, getColorV8 } from "@zendeskgarden/react-theming";
import { MD } from "@zendeskgarden/react-typography";
import styled from "styled-components";
import ClarificationCommentForm from "./ClarificationCommentForm";
import { MAX_COMMENTS } from "./constants";
import type {
ApprovalClarificationFlowMessage,
ApprovalRequest,
} from "../../../types";
import { buildCommentEntityKey } from "./utils";
import { useGetUnreadComments } from "./hooks/useGetUnreadComments";
import NewCommentIndicator from "./NewCommentIndicator";
import ClarificationComment from "./ClarificationComment";
import CommentLimitAlert from "./CommentLimitAlert";
interface ClarificationContainerProps {
approvalRequestId: string;
baseLocale: string;
clarificationFlowMessages: ApprovalClarificationFlowMessage[];
createdByUserId: number;
currentUserAvatarUrl: string;
currentUserId: number;
currentUserName: string;
hasUserViewedBefore: boolean;
status: ApprovalRequest["status"];
}
const Container = styled.div<{ showCommentHeader: boolean }>`
display: flex;
flex-direction: column;
border-top: ${({ showCommentHeader, theme }) =>
showCommentHeader
? `1px solid ${getColor({ theme, hue: "grey", shade: 200 })}`
: "none"};
padding-top: 16px;
`;
const CommentListArea = styled.div`
flex: 1 1 auto;
`;
const ClarificationContent = styled(MD)`
padding: ${({ theme }) => theme.space.xxs} 0;
overflow-wrap: break-word;
white-space: normal;
`;
const TitleAndDescriptionContainer = styled.div`
padding-bottom: 16px;
`;
const StyledDescription = styled(MD)`
padding-top: ${({ theme }) => theme.space.xxs};
color: ${(props) => getColorV8("grey", 600, props.theme)};
`;
export default function ClarificationContainer({
approvalRequestId,
baseLocale,
clarificationFlowMessages,
createdByUserId,
currentUserAvatarUrl,
currentUserId,
currentUserName,
hasUserViewedBefore,
status,
}: ClarificationContainerProps) {
const copy = useGetClarificationCopy();
const hasComments =
clarificationFlowMessages && clarificationFlowMessages.length > 0;
const isTerminalStatus =
!!status &&
(status === "withdrawn" || status === "approved" || status === "rejected");
const canComment =
!isTerminalStatus && clarificationFlowMessages!.length < MAX_COMMENTS;
const showCommentHeader = !isTerminalStatus || hasComments;
const {
unreadComments,
firstUnreadCommentKey,
markCommentAsVisible,
markAllCommentsAsRead,
} = useGetUnreadComments({
comments: clarificationFlowMessages,
currentUserId,
approvalRequestId,
});
useEffect(() => {
const handleBeforeUnload = () => {
markAllCommentsAsRead();
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [markAllCommentsAsRead]);
return (
<Container showCommentHeader={showCommentHeader}>
{showCommentHeader && (
<TitleAndDescriptionContainer>
<MD isBold>{copy.title}</MD>
{canComment && (
<StyledDescription>{copy.description}</StyledDescription>
)}
</TitleAndDescriptionContainer>
)}
<CommentListArea data-testid="comment-list-area">
{hasComments &&
clarificationFlowMessages.map((comment) => {
const commentKey = buildCommentEntityKey(
approvalRequestId,
comment
);
const isFirstUnread = commentKey === firstUnreadCommentKey;
const unreadCount = unreadComments.length;
return (
<React.Fragment key={`${comment.id}`}>
{!isTerminalStatus &&
unreadCount > 0 &&
isFirstUnread &&
hasUserViewedBefore && (
<NewCommentIndicator unreadCount={unreadCount} />
)}
<ClarificationContent key={comment.id}>
<ClarificationComment
baseLocale={baseLocale}
comment={comment}
commentKey={commentKey}
createdByUserId={createdByUserId}
markCommentAsVisible={markCommentAsVisible}
>
{comment.message}
</ClarificationComment>
</ClarificationContent>
</React.Fragment>
);
})}
</CommentListArea>
<CommentLimitAlert
approvalRequestId={approvalRequestId}
commentCount={clarificationFlowMessages!.length}
currentUserId={currentUserId}
isTerminalStatus={isTerminalStatus}
/>
{canComment && (
<ClarificationCommentForm
baseLocale={baseLocale}
currentUserAvatarUrl={currentUserAvatarUrl}
currentUserName={currentUserName}
markAllCommentsAsRead={markAllCommentsAsRead}
/>
)}
</Container>
);
}

View File

@@ -0,0 +1,94 @@
import type React from "react";
import { useState, useCallback } from "react";
import { Alert, Title, Close } from "@zendeskgarden/react-notifications";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_COMMENTS, NEAR_COMMENTS_LIMIT_THRESHOLD } from "./constants";
const StyledAlert = styled(Alert)`
margin-top: ${({ theme }) => theme.space.md};
padding: ${({ theme }) => theme.space.md} ${({ theme }) => theme.space.xl};
`;
interface CommentLimitAlertProps {
approvalRequestId: string;
commentCount: number;
currentUserId: number;
isTerminalStatus: boolean;
}
const CommentLimitAlert: React.FC<CommentLimitAlertProps> = ({
approvalRequestId,
commentCount,
currentUserId,
isTerminalStatus,
}) => {
const { t } = useTranslation();
const alertDismissalKey = `nearLimitAlertDismissed_${currentUserId}_${approvalRequestId}`;
const [nearLimitAlertVisible, setNearLimitAlertVisible] = useState(() => {
return localStorage.getItem(alertDismissalKey) !== "true";
});
const isAtMaxComments = commentCount >= MAX_COMMENTS;
const isNearLimit =
commentCount >= MAX_COMMENTS - NEAR_COMMENTS_LIMIT_THRESHOLD &&
commentCount < MAX_COMMENTS &&
nearLimitAlertVisible;
const commentsRemaining = MAX_COMMENTS - commentCount;
const handleCloseAlert = useCallback(() => {
localStorage.setItem(alertDismissalKey, "true");
setNearLimitAlertVisible(false);
}, [alertDismissalKey]);
if (!isTerminalStatus) {
if (isAtMaxComments) {
return (
<StyledAlert type="info">
<Title>
{t(
"txt.approval_requests.clarification.max_comment_alert_title",
"Comment limit reached"
)}
</Title>
{t(
"txt.approval_requests.clarification.max_comment_alert_message",
"You can't add more comments, approvers can still approve or deny."
)}
</StyledAlert>
);
} else if (isNearLimit) {
return (
<StyledAlert type="info">
<Title>
{t(
"txt.approval_requests.clarification.near_comment_limit_alert_title",
"Comment limit nearly reached"
)}
</Title>
{
(t(
"txt.approval_requests.panel.single_approval_request.clarification.near_comment_limit_alert_message",
{
current_count: commentCount,
remaining_count: commentsRemaining,
}
),
`This request has ${commentCount} of 40 comments available. You have ${commentsRemaining} remaining.`)
}
<Close
onClick={handleCloseAlert}
aria-label={t(
"txt.approval_requests.panel.single_approval_request.clarification.close_alert_button_aria_label",
"Close alert"
)}
/>
</StyledAlert>
);
}
}
return null;
};
export default CommentLimitAlert;

View File

@@ -0,0 +1,52 @@
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { getColor } from "@zendeskgarden/react-theming";
const StyledNewCommentIndicator = styled.div`
align-items: center;
color: ${({ theme }) => getColor({ theme, hue: "red", shade: 600 })};
display: flex;
font-size: ${({ theme }) => theme.fontSizes.md};
text-align: center;
padding-top: ${({ theme }) => theme.space.sm};
padding-bottom: ${({ theme }) => theme.space.xxs};
&:before,
&:after {
content: "";
flex: 1;
border-bottom: 1px solid
${(props) => getColor({ theme: props.theme, hue: "red", shade: 600 })};
}
&:before {
margin-right: 16px;
}
&:after {
margin-left: 16px;
}
`;
interface NewCommentIndicatorProps {
unreadCount: number;
}
function NewCommentIndicator({ unreadCount }: NewCommentIndicatorProps) {
const { t } = useTranslation();
return (
<StyledNewCommentIndicator>
{unreadCount === 1
? t(
"txt.approval_requests.clarification.new_comment_indicator",
"New comment"
)
: t(
"txt.approval_requests.clarification.new_comments_indicator",
"New comments"
)}
</StyledNewCommentIndicator>
);
}
export default NewCommentIndicator;

View File

@@ -0,0 +1,154 @@
import type React from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
isSameDay as dfIsSameDay,
subDays,
isSameYear,
differenceInMinutes,
differenceInSeconds,
} from "date-fns";
import styled from "styled-components";
import { getColor } from "@zendeskgarden/react-theming";
import { SM } from "@zendeskgarden/react-typography";
const StyledTimestamp = styled(SM)`
color: ${({ theme }) => getColor({ theme, hue: "grey", shade: 600 })};
align-self: center;
justify-content: center;
padding-top: 1px;
`;
const isSameDay = (date1: Date, date2: Date) => dfIsSameDay(date1, date2);
const isYesterday = (date: Date, now: Date) => {
const yesterday = subDays(now, 1);
return dfIsSameDay(date, yesterday);
};
function detectHour12(locale: string): boolean {
const dtf = new Intl.DateTimeFormat(locale, { hour: "numeric" });
const options = dtf.resolvedOptions();
return options.hour12 ?? true;
}
const formatDate = (date: Date, locale: string) =>
date.toLocaleDateString(locale, { month: "short", day: "2-digit" });
const formatDateWithYear = (date: Date, locale: string) =>
date.toLocaleDateString(locale, {
month: "short",
day: "2-digit",
year: "numeric",
});
const formatTime = (date: Date, locale: string, hour12: boolean) =>
date.toLocaleTimeString(locale, {
hour: "numeric",
minute: "2-digit",
hour12,
});
interface RelativeTimeProps {
eventTime: string;
locale: string;
}
export const RelativeTime: React.FC<RelativeTimeProps> = ({
eventTime,
locale,
}) => {
const { t } = useTranslation();
const hour12 = useMemo(() => detectHour12(locale), [locale]);
const now = new Date();
const eventDate = new Date(eventTime);
if (isNaN(eventDate.getTime())) return null;
const elapsedSeconds = differenceInSeconds(now, eventDate);
const elapsedMinutes = differenceInMinutes(now, eventDate);
if (elapsedSeconds < 0 || elapsedSeconds < 60) {
return (
<StyledTimestamp>
{
(t("approval_request.clarification.timestamp_lessThanAMinuteAgo"),
`< 1 minute ago`)
}
</StyledTimestamp>
);
}
if (elapsedMinutes < 60) {
const pluralRules = new Intl.PluralRules(locale);
const plural = pluralRules.select(elapsedMinutes);
return (
<StyledTimestamp>
{
(t("approval_request.clarification.timestamp_minutesAgo", {
count: elapsedMinutes,
plural,
}),
`${elapsedMinutes} minute${plural === "one" ? "" : "s"} ago`)
}
</StyledTimestamp>
);
}
const timeStr = formatTime(eventDate, locale, hour12);
if (isSameDay(eventDate, now)) {
return (
<StyledTimestamp>
{
(t("approval_request.clarification.timestamp_todayAt", {
time: timeStr,
}),
`Today at ${timeStr}`)
}
</StyledTimestamp>
);
}
if (isYesterday(eventDate, now)) {
return (
<StyledTimestamp>
{
(t("approval_request.clarification.timestamp_yesterdayAt", {
time: timeStr,
}),
`Yesterday at ${timeStr}`)
}
</StyledTimestamp>
);
}
if (isSameYear(eventDate, now)) {
const dateStr = formatDate(eventDate, locale);
return (
<StyledTimestamp>
{
(t("approval_request.clarification.timestamp_dateAt", {
date: dateStr,
time: timeStr,
}),
`${dateStr} at ${timeStr}`)
}
</StyledTimestamp>
);
}
const dateStr = formatDateWithYear(eventDate, locale);
return (
<StyledTimestamp>
{
(t("approval_request.clarification.timestamp_dateAt", {
date: dateStr,
time: timeStr,
}),
`${dateStr} at ${timeStr}`)
}
</StyledTimestamp>
);
};
export default RelativeTime;

View File

@@ -0,0 +1,6 @@
export const MAX_COMMENTS = 40;
export const NEAR_COMMENTS_LIMIT_THRESHOLD = 5;
export const MAX_CHAR_COUNT = 500;
export const WARNING_THRESHOLD = 10;
export const DEFAULT_AVATAR_URL =
"https://secure.gravatar.com/avatar/6d713fed56e4dd3e48f6b824b8789d7f?default=https%3A%2F%2Fassets.zendesk.com%2Fhc%2Fassets%2Fdefault_avatar.png&r=g";

View File

@@ -0,0 +1,393 @@
import type React from "react";
import { useCommentForm } from "../useCommentForm";
import { MAX_CHAR_COUNT, WARNING_THRESHOLD } from "../../constants";
import { act, renderHook } from "@testing-library/react-hooks";
const mockEvent = (val: string) => {
return {
target: { value: val },
currentTarget: {
value: val,
},
preventDefault: jest.fn(),
} as unknown as React.ChangeEvent<HTMLTextAreaElement>;
};
const mockKeyboardEvent = (
key: string,
options = {}
): React.KeyboardEvent<HTMLTextAreaElement> => {
return {
key,
shiftKey: false,
ctrlKey: false,
metaKey: false,
preventDefault: jest.fn(),
...options,
} as unknown as React.KeyboardEvent<HTMLTextAreaElement>;
};
const mockMarkAllCommentsAsRead = jest.fn();
describe("useCommentForm", () => {
const baseLocale = "en-US";
const successOnSubmit = jest.fn(() => Promise.resolve());
const markAllCommentsAsRead = mockMarkAllCommentsAsRead;
beforeEach(() => {
jest.clearAllMocks();
});
it("should return initial values", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
const { comment, commentValidation, charLimitMessage, isInputFocused } =
result.current;
expect(comment).toBe("");
expect(commentValidation).toBeUndefined();
expect(charLimitMessage).toBe("");
expect(isInputFocused).toBe(false);
});
describe("validation", () => {
it("should not submit when input is empty", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange(mockEvent(""));
result.current.handleSubmit();
});
expect(result.current.comment).toBe("");
expect(result.current.commentValidation).toBe("error");
expect(successOnSubmit).not.toHaveBeenCalled();
});
it("should submit when input is not empty", async () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange(mockEvent("t"));
});
expect(result.current.comment).toBe("t");
await act(async () => {
await result.current.handleSubmit();
});
expect(result.current.comment).toBe(""); // after submit comment is cleared
expect(result.current.commentValidation).toBe(undefined);
expect(successOnSubmit).toHaveBeenCalled();
});
it("should handleBlur", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleFocus();
});
expect(result.current.isInputFocused).toBe(true);
const mockFocusEvent = {
target: {},
currentTarget: {},
preventDefault: jest.fn(),
} as unknown as React.FocusEvent<HTMLTextAreaElement>;
act(() => {
result.current.handleBlur(mockFocusEvent);
});
expect(result.current.isInputFocused).toBe(false);
});
it("should submit when input is not empty and user clicks enter", async () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange(mockEvent("t"));
});
const enterEvent = mockKeyboardEvent("Enter", { shiftKey: false });
await act(async () => {
await result.current.handleKeyDown(enterEvent);
});
expect(result.current.comment).toBe("");
expect(result.current.commentValidation).toBe(undefined);
expect(successOnSubmit).toHaveBeenCalled();
});
it("should show charLimitMessage warning when 10 characters remaining", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
const inputWith10Remaining = "a".repeat(
MAX_CHAR_COUNT - WARNING_THRESHOLD
);
result.current.handleChange(mockEvent(inputWith10Remaining));
});
expect(result.current.commentValidation).toBe("warning");
expect(result.current.charLimitMessage).toContain(
"10 characters remaining"
);
});
it("should update charLimitMessage and validation warning with 1 character remaining", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
const inputWithOneRemaining = "a".repeat(499);
result.current.handleChange(mockEvent(inputWithOneRemaining));
});
expect(result.current.comment.length).toBe(499);
expect(result.current.commentValidation).toBe("warning");
expect(result.current.charLimitMessage).toContain(
"1 character remaining"
);
});
it("should truncate input exceeding max length", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
const longInput = "a".repeat(MAX_CHAR_COUNT + 10);
result.current.handleChange(mockEvent(longInput));
});
expect(result.current.comment.length).toBe(MAX_CHAR_COUNT);
});
it("should clear error validation when user types valid input after error", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleSubmit();
});
act(() => {
result.current.handleChange(mockEvent("a"));
});
expect(result.current.commentValidation).toBeUndefined();
});
it("calls markAllCommentsAsRead after successful submit", async () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange(mockEvent("valid comment"));
});
await act(async () => {
await result.current.handleSubmit();
});
expect(mockMarkAllCommentsAsRead).toHaveBeenCalledTimes(1);
});
describe("handleKeyDown", () => {
it("does not submit on Enter with Shift (allows newline)", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
const enterShiftEvent = mockKeyboardEvent("Enter", { shiftKey: true });
act(() => {
result.current.handleKeyDown(enterShiftEvent);
});
expect(enterShiftEvent.preventDefault).not.toHaveBeenCalled();
expect(successOnSubmit).not.toHaveBeenCalled();
});
it("submits on Enter", async () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange(mockEvent("t"));
});
const enterEvent = mockKeyboardEvent("Enter");
await act(async () => {
result.current.handleKeyDown(enterEvent);
});
expect(successOnSubmit).toHaveBeenCalled();
});
it("cancels on Escape key", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
const escapeEvent = mockKeyboardEvent("Escape");
act(() => {
result.current.handleKeyDown(escapeEvent);
});
expect(escapeEvent.preventDefault).toHaveBeenCalled();
const { comment, commentValidation, charLimitMessage, isInputFocused } =
result.current;
expect(comment).toBe("");
expect(commentValidation).toBeUndefined();
expect(charLimitMessage).toBe("");
expect(isInputFocused).toBe(false);
});
it("prevents input when max length reached and non-allowed key pressed", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange({
target: { value: "a".repeat(MAX_CHAR_COUNT) },
} as React.ChangeEvent<HTMLTextAreaElement>);
});
const keyEvent = mockKeyboardEvent("b");
act(() => {
result.current.handleKeyDown(keyEvent);
});
expect(keyEvent.preventDefault).toHaveBeenCalled();
});
it("allows navigation keys and shortcuts at max length", () => {
const { result } = renderHook(() =>
useCommentForm({
onSubmit: successOnSubmit,
baseLocale,
markAllCommentsAsRead,
})
);
act(() => {
result.current.handleChange({
target: { value: "a".repeat(MAX_CHAR_COUNT) },
} as React.ChangeEvent<HTMLTextAreaElement>);
});
const allowedKeys = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Tab",
"Home",
"End",
];
const shortcuts = [
{ key: "c", ctrlKey: true },
{ key: "a", metaKey: true },
{ key: "x", ctrlKey: true },
{ key: "z", ctrlKey: true },
];
allowedKeys.forEach((key) => {
const event = mockKeyboardEvent(key);
act(() => {
result.current.handleKeyDown(event);
});
expect(event.preventDefault).not.toHaveBeenCalled();
});
shortcuts.forEach(({ key, ctrlKey, metaKey }) => {
const event = mockKeyboardEvent(key, { ctrlKey, metaKey });
act(() => {
result.current.handleKeyDown(event);
});
expect(event.preventDefault).not.toHaveBeenCalled();
});
});
});
});
});

View File

@@ -0,0 +1,158 @@
import { renderHook } from "@testing-library/react-hooks";
import { act } from "@testing-library/react";
import { useGetUnreadComments } from "../useGetUnreadComments";
import type { ApprovalClarificationFlowMessage } from "../../../../../types";
import { buildCommentEntityKey } from "../../utils";
describe("useGetUnreadComments with real buildCommentEntityKey", () => {
const approvalRequestId = "123";
const currentUserId = 456;
const comment1: ApprovalClarificationFlowMessage = {
id: "comment1-id",
author: {
id: currentUserId,
email: "currentuser@example.com",
avatar: "",
name: "Current User",
},
message: "My own comment",
created_at: "2024-06-01T10:00:00Z",
};
const comment2: ApprovalClarificationFlowMessage = {
id: "comment2-id",
author: {
id: 567,
email: "jane@example.com",
avatar: "",
name: "Jane Doe",
},
message: "First comment",
created_at: "2024-06-02T11:00:00Z",
};
const comment3: ApprovalClarificationFlowMessage = {
id: "comment3-id",
author: {
id: 678,
email: "john@example.com",
avatar: "",
name: "John Doe",
},
message: "Second comment",
created_at: "2024-06-03T12:00:00Z",
};
const generateCommentKey = (comment: ApprovalClarificationFlowMessage) =>
buildCommentEntityKey(approvalRequestId, comment);
beforeEach(() => {
window.localStorage.clear();
});
it("filters unread comments correctly and returns firstUnreadCommentKey", () => {
const comments = [comment1, comment2, comment3];
const { result } = renderHook(() =>
useGetUnreadComments({
comments,
currentUserId,
approvalRequestId,
})
);
// comment1 is currentUser's and filtered out, only comment2 and comment3 considered unread
expect(result.current.unreadComments.length).toBe(2);
expect(result.current.unreadComments).toContainEqual(comment2);
expect(result.current.unreadComments).toContainEqual(comment3);
expect(result.current.firstUnreadCommentKey).toBe(
generateCommentKey(comment2)
);
});
it("markCommentAsVisible sets the comment visible in storage and state", () => {
const comments = [comment2];
const storageKey = `readComments:${currentUserId}:${approvalRequestId}`;
const commentKey = generateCommentKey(comment2);
const { result } = renderHook(() =>
useGetUnreadComments({
comments,
currentUserId,
approvalRequestId,
})
);
act(() => {
result.current.markCommentAsVisible(commentKey);
});
const stored = JSON.parse(window.localStorage.getItem(storageKey) || "{}");
expect(stored[commentKey]).toEqual({ visible: true, read: false });
});
it("markAllCommentsAsRead marks all visible comments as read", () => {
const comments = [comment2, comment3];
const storageKey = `readComments:${currentUserId}:${approvalRequestId}`;
const key1 = generateCommentKey(comment2);
const key2 = generateCommentKey(comment3);
window.localStorage.setItem(
storageKey,
JSON.stringify({
[key1]: { visible: true, read: false },
[key2]: { visible: true, read: false },
})
);
const { result } = renderHook(() =>
useGetUnreadComments({
comments,
currentUserId,
approvalRequestId,
})
);
act(() => {
result.current.markAllCommentsAsRead();
});
const stored = JSON.parse(window.localStorage.getItem(storageKey) || "{}");
expect(stored[key1]).toEqual({ visible: true, read: true });
expect(stored[key2]).toEqual({ visible: true, read: true });
});
it("markAllCommentsAsRead does not mark comments as read if visible is false", () => {
const comments = [comment2, comment3];
const storageKey = `readComments:${currentUserId}:${approvalRequestId}`;
const key2 = generateCommentKey(comment2);
const key3 = generateCommentKey(comment3);
window.localStorage.setItem(
storageKey,
JSON.stringify({
[key2]: { visible: false, read: false },
[key3]: { visible: true, read: false },
})
);
const { result } = renderHook(() =>
useGetUnreadComments({
comments,
currentUserId,
approvalRequestId,
})
);
act(() => {
result.current.markAllCommentsAsRead();
});
const stored = JSON.parse(window.localStorage.getItem(storageKey) || "{}");
expect(stored[key2]).toEqual({ visible: false, read: false });
expect(stored[key3]).toEqual({ visible: true, read: true });
});
});

View File

@@ -0,0 +1,54 @@
import { renderHook } from "@testing-library/react-hooks";
import { act } from "@testing-library/react";
import { useIntersectionObserver } from "../useIntersectionObserver";
describe("useIntersectionObserver", () => {
let observeMock: jest.Mock;
let unobserveMock: jest.Mock;
beforeEach(() => {
observeMock = jest.fn();
unobserveMock = jest.fn();
class IntersectionObserverMock {
callback: IntersectionObserverCallback;
constructor(callback: IntersectionObserverCallback) {
this.callback = callback;
}
observe = observeMock;
unobserve = unobserveMock;
disconnect = jest.fn();
takeRecords = jest.fn();
}
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
configurable: true,
value: IntersectionObserverMock,
});
});
it("observes element and calls callback on intersect", () => {
const mockCallback = jest.fn();
const ref = { current: document.createElement("div") };
const { unmount } = renderHook(() =>
useIntersectionObserver(ref as React.RefObject<Element>, mockCallback)
);
expect(observeMock).toHaveBeenCalledWith(ref.current);
act(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const observerInstance = observeMock.mock.instances[0] as any;
observerInstance.callback([
{ target: ref.current, isIntersecting: true },
]);
});
expect(mockCallback).toHaveBeenCalledTimes(1);
unmount();
expect(unobserveMock).toHaveBeenCalledWith(ref.current);
});
});

View File

@@ -0,0 +1,12 @@
import { renderHook } from "@testing-library/react-hooks";
import { useSubmitComment } from "../useSubmitComment";
describe("useSubmitComment", () => {
it("should return initial values", () => {
const { result } = renderHook(() => useSubmitComment());
const { handleSubmitComment, isLoading } = result.current;
expect(handleSubmitComment).toBeDefined();
expect(isLoading).toBe(false);
});
});

View File

@@ -0,0 +1,182 @@
import type React from "react";
import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { MAX_CHAR_COUNT, WARNING_THRESHOLD } from "../constants";
export const useCommentForm = ({
onSubmit,
baseLocale,
markAllCommentsAsRead,
}: {
onSubmit: (comment: string) => Promise<unknown>;
baseLocale: string;
markAllCommentsAsRead: () => void;
}) => {
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const buttonsContainerRef = useRef<HTMLDivElement | null>(null);
const [comment, setComment] = useState<string>("");
const [commentValidation, setCommentValidation] = useState<
undefined | "error" | "warning"
>();
const [isInputFocused, setIsInputFocused] = useState(false);
const [charLimitMessage, setCharLimitMessage] = useState("");
const { t } = useTranslation();
const validateComment = (value: string) => {
const isValid = value.trim().length > 0;
return isValid;
};
const handleCancel = () => {
setComment("");
setCommentValidation(undefined);
setCharLimitMessage("");
setIsInputFocused(false);
// Blur textarea to remove focus; state update alone doesnt blur DOM element.
textareaRef.current?.blur();
};
const handleBlur = (e?: React.FocusEvent<HTMLTextAreaElement>) => {
// Ignore blur if focus is moving to the buttons to keep validation UI visible,
// especially when the user submits an empty comment.
const relatedTarget = e?.relatedTarget;
if (
relatedTarget &&
(buttonsContainerRef.current?.contains(relatedTarget) ?? false)
) {
return;
}
setIsInputFocused(false);
};
const handleFocus = () => {
setIsInputFocused(true);
};
const handleSubmit = useCallback(async () => {
const isValid = validateComment(comment);
if (isValid) {
try {
await onSubmit(comment);
markAllCommentsAsRead();
// clear form
handleCancel();
return true;
} catch (error) {
console.error(error);
return false;
}
} else {
setCommentValidation("error");
textareaRef.current?.focus();
return false;
}
}, [comment, markAllCommentsAsRead, onSubmit]);
const handleKeyDown = useCallback(
async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const allowedKeys = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Tab",
"Enter",
"Escape",
"Home",
"End",
];
const isCopyShortcut =
(e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "c";
const isSelectAllShortcut =
(e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a";
const isCutShortcut =
(e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "x";
const isUndoShortcut =
(e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z";
// Block input beyond max char count and allowed certain keys and shortcuts
if (
comment.length >= MAX_CHAR_COUNT &&
!allowedKeys.includes(e.key) &&
!isCopyShortcut &&
!isSelectAllShortcut &&
!isCutShortcut &&
!isUndoShortcut
) {
e.preventDefault();
return;
}
if (e.key === "Enter" && e.shiftKey === false) {
e.preventDefault();
await handleSubmit();
} else if (e.key === "Escape") {
e.preventDefault();
handleCancel();
}
},
[comment.length, handleSubmit]
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
let input = e.target.value;
// Only update state if input length is within max limit
if (input.length > MAX_CHAR_COUNT) {
input = input.substring(0, MAX_CHAR_COUNT);
}
setComment(input);
setIsInputFocused(true);
const charsLeft = MAX_CHAR_COUNT - input.length;
if (charsLeft >= 0 && charsLeft <= WARNING_THRESHOLD) {
const pluralRules = new Intl.PluralRules(baseLocale);
const plural = pluralRules.select(charsLeft);
setCommentValidation("warning");
setCharLimitMessage(
t(`txt.approval_requests.validation.characters_remaining.${plural}`, {
numCharacters: charsLeft,
defaultValue: `${charsLeft} character${
plural === "one" ? "" : "s"
} remaining`,
})
);
} else if (commentValidation === "warning") {
// Clear warning if no longer near limit and warning was previously shown
setCommentValidation(undefined);
setCharLimitMessage("");
}
// Clear error if user starts typing valid input after error was shown
if (commentValidation === "error" && input.trim().length > 0) {
setCommentValidation(undefined);
}
},
[baseLocale, commentValidation, t]
);
return {
textareaRef,
buttonsContainerRef,
charLimitMessage,
comment,
commentValidation,
isInputFocused,
setComment,
handleBlur,
handleCancel,
handleChange,
handleFocus,
handleKeyDown,
handleSubmit,
};
};

View File

@@ -0,0 +1,27 @@
import { useTranslation } from "react-i18next";
export const useGetClarificationCopy = () => {
const { t } = useTranslation();
return {
title: t("txt.approval_requests.clarification.title", "Comments"),
description: t(
"txt.approval_requests.clarification.description",
"Add notes or ask for additional information about this request"
),
comment_form_aria_label: t(
"txt.approval_requests.clarification.comment_form_aria_label",
"Enter a comment to ask for additional information about this approval request"
),
submit_button: t(
"txt.approval_requests.clarification.submit_button",
"Send"
),
cancel_button: t(
"txt.approval_requests.clarification.cancel_button",
"Cancel"
),
validation_empty_input: t(
"txt.approval_requests.clarification.validation_empty_comment_error",
"Enter a comment"
),
};
};

View File

@@ -0,0 +1,121 @@
import { useEffect, useState, useMemo, useCallback } from "react";
import { buildCommentEntityKey } from "../utils";
import type { ApprovalClarificationFlowMessage } from "../../../../types";
interface UseGetUnreadCommentsParams {
approvalRequestId: string;
comments: ApprovalClarificationFlowMessage[];
currentUserId: number;
storageKeyPrefix?: string;
}
interface CommentReadState {
read: boolean;
visible?: boolean;
}
interface UseGetUnreadCommentsResult {
firstUnreadCommentKey: string | null;
markCommentAsVisible: (commentKey: string) => void;
markAllCommentsAsRead: () => void;
unreadComments: ApprovalClarificationFlowMessage[];
}
export function useGetUnreadComments({
comments,
currentUserId,
approvalRequestId,
}: UseGetUnreadCommentsParams): UseGetUnreadCommentsResult {
const storage = window.localStorage;
const localStorageKey = `readComments:${currentUserId}:${approvalRequestId}`;
const getLocalReadStates = useCallback((): Record<
string,
CommentReadState
> => {
try {
const stored = storage.getItem(localStorageKey);
return stored ? JSON.parse(stored) : {};
} catch {
return {};
}
}, [localStorageKey, storage]);
const setLocalReadStates = useCallback(
(states: Record<string, CommentReadState>) => {
try {
storage.setItem(localStorageKey, JSON.stringify(states));
} catch {
// Ignore storage errors
}
},
[localStorageKey, storage]
);
const [localReadStates, setLocalReadStatesState] = useState<
Record<string, CommentReadState>
>(() => getLocalReadStates());
// Re-sync local read states when approvalRequestId changes
useEffect(() => {
setLocalReadStatesState(getLocalReadStates());
}, [approvalRequestId, getLocalReadStates]);
const markCommentAsVisible = (commentKey: string) => {
const currentStates = { ...localReadStates };
if (!currentStates[commentKey]?.visible) {
currentStates[commentKey] = {
...currentStates[commentKey],
visible: true,
read: currentStates[commentKey]?.read ?? false,
};
setLocalReadStates(currentStates);
setLocalReadStatesState(currentStates);
}
};
const markAllCommentsAsRead = useCallback(() => {
setLocalReadStatesState((prev) => {
const newStates = { ...prev };
Object.keys(newStates).forEach((key) => {
if (newStates[key]?.visible) {
newStates[key] = {
...newStates[key],
read: true,
};
}
});
setLocalReadStates(newStates);
return newStates;
});
}, [setLocalReadStates]);
// Compute unread comments filtering out current user's comments
const { unreadComments, firstUnreadCommentKey } = useMemo(() => {
const filtered = comments.filter(
(comment) => String(comment.author.id) !== String(currentUserId)
);
const unread = filtered.filter((comment) => {
const key = buildCommentEntityKey(approvalRequestId, comment);
const state = localReadStates[key];
return !state?.read;
});
const firstUnreadKey = unread[0]
? buildCommentEntityKey(approvalRequestId, unread[0])
: null;
return { unreadComments: unread, firstUnreadCommentKey: firstUnreadKey };
}, [comments, localReadStates, currentUserId, approvalRequestId]);
return {
firstUnreadCommentKey,
markCommentAsVisible,
markAllCommentsAsRead,
unreadComments,
};
}

View File

@@ -0,0 +1,55 @@
import { useEffect, useCallback } from "react";
const observedElements = new Map<Element, () => void>();
let observer: IntersectionObserver | null = null;
const getObserver = () => {
if (observer) return observer;
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const elementCallback = observedElements.get(entry.target);
if (elementCallback) {
elementCallback();
observer?.unobserve(entry.target);
observedElements.delete(entry.target);
}
}
});
},
{ threshold: 1.0 }
);
return observer;
};
export const useIntersectionObserver = (
ref: React.RefObject<Element>,
markCommentAsVisible: () => void
) => {
const observe = useCallback(() => {
const element = ref.current;
if (!element) return;
const ob = getObserver();
observedElements.set(element, markCommentAsVisible);
ob.observe(element);
}, [ref, markCommentAsVisible]);
const unobserve = useCallback(() => {
const element = ref.current;
if (!element || !observer) return;
observer.unobserve(element);
observedElements.delete(element);
}, [ref]);
useEffect(() => {
observe();
return () => unobserve();
}, [observe, unobserve]);
};

View File

@@ -0,0 +1,20 @@
import { useState } from "react";
export const useSubmitComment = () => {
const [isLoading, setIsLoading] = useState(false);
const handleSubmitComment = async (comment: string) => {
setIsLoading(true);
try {
console.log("Submitting comment:", comment);
await new Promise((resolve) => setTimeout(resolve, 500));
return { success: true, message: "Comment logged successfully" };
} finally {
setIsLoading(false);
}
};
return { handleSubmitComment, isLoading };
};

View File

@@ -0,0 +1,123 @@
import { screen } from "@testing-library/react";
import ClarificationComment from "../ClarificationComment";
import { type ApprovalClarificationFlowMessage } from "../../../../types";
import { renderWithTheme } from "../../../../testHelpers";
jest.mock("@zendeskgarden/svg-icons/src/16/headset-fill.svg", () => "svg-mock");
jest.mock(
"@zendeskgarden/svg-icons/src/12/circle-sm-fill.svg",
() => "svg-mock"
);
describe("ClarificationComment", () => {
const mockComment: ApprovalClarificationFlowMessage = {
id: "comment-id-1",
author: {
id: 123,
email: `user@example.com`,
avatar: "https://example.com/avatar.png",
name: "John Doe",
},
message: "This is a test comment.",
created_at: "2024-01-01T12:00:00Z",
} as ApprovalClarificationFlowMessage;
const mockCommentKey = "some-unique-comment-key";
const mockMarkCommentAsVisible = jest.fn();
const intersectionObserverMock = {
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
takeRecords: jest.fn(),
};
class IntersectionObserverMock {
callback: IntersectionObserverCallback;
constructor(callback: IntersectionObserverCallback) {
this.callback = callback;
}
observe = jest.fn((element: Element) => {
this.callback(
[
{
isIntersecting: true,
target: element,
} as IntersectionObserverEntry,
],
this as unknown as IntersectionObserver
);
intersectionObserverMock.observe(element);
});
unobserve = intersectionObserverMock.unobserve;
disconnect = intersectionObserverMock.disconnect;
takeRecords = intersectionObserverMock.takeRecords;
}
const props = {
baseLocale: "en-US",
comment: mockComment,
commentKey: mockCommentKey,
markCommentAsVisible: mockMarkCommentAsVisible,
children: mockComment.message,
createdByUserId: 999,
};
beforeAll(() => {
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
configurable: true,
value: IntersectionObserverMock,
});
});
it("calls markCommentAsVisible when comment becomes visible", () => {
renderWithTheme(<ClarificationComment {...props} />);
expect(mockMarkCommentAsVisible).toHaveBeenCalledTimes(1);
expect(mockMarkCommentAsVisible).toHaveBeenCalledWith(mockCommentKey);
});
it("renders the author name", () => {
renderWithTheme(<ClarificationComment {...props} />);
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
it("renders the comment body", () => {
renderWithTheme(<ClarificationComment {...props} />);
expect(screen.getByText("This is a test comment.")).toBeInTheDocument();
});
it("renders the author avatar", () => {
renderWithTheme(<ClarificationComment {...props} />);
const avatar = screen.getByRole("img", { name: /John doe/i });
expect(avatar).toBeInTheDocument();
});
it("handles empty children gracefully", () => {
renderWithTheme(
<ClarificationComment
baseLocale="en-US"
comment={mockComment}
commentKey={mockCommentKey}
markCommentAsVisible={mockMarkCommentAsVisible}
createdByUserId={999}
/>
);
expect(
screen.queryByText("This is a test comment.")
).not.toBeInTheDocument();
});
it("displays message", () => {
renderWithTheme(<ClarificationComment {...props} />);
expect(screen.queryByText("This is a test comment.")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,178 @@
import { screen, waitFor, act, fireEvent } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import ClarificationCommentForm from "../ClarificationCommentForm";
import { useSubmitComment } from "../hooks/useSubmitComment";
import { useGetClarificationCopy } from "../hooks/useGetClarificationCopy";
import { MAX_CHAR_COUNT, WARNING_THRESHOLD } from "../constants";
import { renderHook } from "@testing-library/react-hooks";
import { renderWithTheme } from "../../../../testHelpers";
jest.mock("../hooks/useSubmitComment");
describe("ClarificationCommentForm", () => {
const mockMarkAllCommentsAsRead = jest.fn();
const props = {
baseLocale: "en-US",
currentUserAvatarUrl: "https://example.com/avatar.png",
currentUserName: "Jane Doe",
markAllCommentsAsRead: mockMarkAllCommentsAsRead,
};
beforeEach(() => {
(useSubmitComment as jest.Mock).mockReturnValue({
status: "",
handleSubmitComment: jest.fn(),
});
});
it("renders the form with a textarea and hidden buttons", () => {
const { result } = renderHook(() => useGetClarificationCopy());
renderWithTheme(<ClarificationCommentForm {...props} />);
expect(screen.getByRole("textbox")).toBeInTheDocument();
// Buttons are hidden initially
expect(
screen.queryByText(result.current.submit_button)
).not.toBeInTheDocument();
expect(
screen.queryByText(result.current.cancel_button)
).not.toBeInTheDocument();
});
it("displays the user avatar", () => {
renderWithTheme(<ClarificationCommentForm {...props} />);
const avatar = screen.getByRole("img", { name: /jane doe/i });
expect(avatar).toBeInTheDocument();
});
it("disables buttons when the form is submitting", async () => {
const { result } = renderHook(() => useGetClarificationCopy());
(useSubmitComment as jest.Mock).mockReturnValue({
isLoading: true,
handleSubmitComment: jest.fn(),
});
renderWithTheme(<ClarificationCommentForm {...props} />);
// Focus textarea to trigger buttons rendering
const textarea = screen.getByRole("textbox");
userEvent.click(textarea);
await waitFor(async () => {
const submitBtn = await screen.findByRole("button", {
name: result.current.submit_button,
});
const cancelBtn = await screen.findByRole("button", {
name: result.current.cancel_button,
});
expect(submitBtn).toBeDisabled();
expect(cancelBtn).toBeDisabled();
});
});
it("calls handleSubmitComment when the submit button is clicked", async () => {
const { result } = renderHook(() => useGetClarificationCopy());
const handleSubmitCommentMock = jest.fn(() =>
Promise.resolve({ status: "success", data: "" })
);
(useSubmitComment as jest.Mock).mockReturnValue({
status: "",
handleSubmitComment: handleSubmitCommentMock,
});
renderWithTheme(<ClarificationCommentForm {...props} />);
await userEvent.type(screen.getByRole("textbox"), "Test comment");
const submitBtn = await screen.findByRole("button", {
name: result.current.submit_button,
});
await act(async () => {
await userEvent.click(submitBtn);
});
expect(handleSubmitCommentMock).toHaveBeenCalledWith("Test comment");
});
it("calls handleCancel when the cancel button is clicked", async () => {
const { result } = renderHook(() => useGetClarificationCopy());
renderWithTheme(<ClarificationCommentForm {...props} />);
const textarea = screen.getByRole("textbox");
await userEvent.type(textarea, "Test comment");
expect(screen.getByText("Test comment")).toBeInTheDocument();
const cancelButton = screen.getByText(result.current.cancel_button);
await act(async () => {
await userEvent.click(cancelButton);
});
expect(screen.queryByText("Test comment")).not.toBeInTheDocument();
});
it("shows validation error when input is empty and submit is clicked", async () => {
const { result } = renderHook(() => useGetClarificationCopy());
renderWithTheme(<ClarificationCommentForm {...props} />);
// Focus textarea to trigger buttons rendering
const textarea = screen.getByRole("textbox");
userEvent.click(textarea);
const sendButton = await screen.findByRole("button", {
name: result.current.submit_button,
});
await userEvent.click(sendButton);
expect(
screen.getByText(result.current.validation_empty_input)
).toBeInTheDocument();
});
it("shows warning message when near max character limit", async () => {
renderWithTheme(<ClarificationCommentForm {...props} />);
const textarea = screen.getByRole("textbox");
const nearMaxLengthText = "a".repeat(MAX_CHAR_COUNT - WARNING_THRESHOLD);
// using fireEvent here because userEvent.type has issues with large text inputs
fireEvent.change(textarea, { target: { value: nearMaxLengthText } });
const warningMessage = screen.getByRole("alert");
expect(warningMessage).toBeInTheDocument();
expect(
screen.getByText(`${WARNING_THRESHOLD} characters remaining`)
).toBeInTheDocument();
});
it("buttons show only when input is focused or has content", async () => {
const { result } = renderHook(() => useGetClarificationCopy());
renderWithTheme(<ClarificationCommentForm {...props} />);
const textarea = screen.getByRole("textbox");
// Initially buttons hidden
expect(
screen.queryByRole("button", { name: result.current.submit_button })
).not.toBeInTheDocument();
userEvent.type(textarea, "Some text");
expect(
await screen.findByRole("button", { name: result.current.submit_button })
).toBeInTheDocument();
});
it("prevents typing beyond max length", () => {
renderWithTheme(<ClarificationCommentForm {...props} />);
const textarea = screen.getByRole("textbox");
const longText = "a".repeat(MAX_CHAR_COUNT + 10);
// using fireEvent here because userEvent.type has issues with large text inputs
fireEvent.change(textarea, { target: { value: longText } });
expect(textarea).toHaveValue(longText.slice(0, MAX_CHAR_COUNT));
});
});

View File

@@ -0,0 +1,227 @@
import { screen, fireEvent, within } from "@testing-library/react";
import ClarificationContainer from "../ClarificationContainer";
import { useGetClarificationCopy } from "../hooks/useGetClarificationCopy";
import { useGetUnreadComments } from "../hooks/useGetUnreadComments";
import { MAX_COMMENTS } from "../constants";
import NewCommentIndicator from "../NewCommentIndicator";
import { buildCommentEntityKey } from "../utils";
import { renderWithTheme } from "../../../../testHelpers";
import { APPROVAL_REQUEST_STATES } from "../../../../constants";
import { renderHook } from "@testing-library/react-hooks";
import type { ApprovalClarificationFlowMessage } from "../../../../types";
jest.mock(
"@zendeskgarden/svg-icons/src/12/circle-sm-fill.svg",
() => "svg-mock"
);
jest.mock("../hooks/useGetUnreadComments");
jest.mock("../NewCommentIndicator");
jest.mock("@zendeskgarden/svg-icons/src/16/headset-fill.svg", () => "svg-mock");
const createClarificationFlowMessage = (
index: number
): ApprovalClarificationFlowMessage => ({
id: `id-${index}`,
author: {
email: `user${index}@example.com`,
id: 456,
avatar: `https://i.pravatar.cc/150?img=${index}`,
name: `User ${index}`,
},
message: `Comment message ${index}`,
created_at: "2024-01-01T12:00:00Z",
});
describe("ClarificationContainer", () => {
const mockMessages = Array.from({ length: 2 }, (_, i) =>
createClarificationFlowMessage(i + 1)
);
const approvalRequestId = "req-123";
const baseLocale = "en-US";
const defaultProps = {
baseLocale,
approvalRequestId,
currentUserId: 456,
createdByUserId: 123,
currentUserName: "Jane Doe",
currentUserAvatarUrl: "https://example.com/avatar.png",
status: APPROVAL_REQUEST_STATES.ACTIVE,
clarificationFlowMessages: mockMessages,
hasUserViewedBefore: true,
};
beforeEach(() => {
(useGetUnreadComments as jest.Mock).mockReturnValue({
unreadComments: [],
firstUnreadCommentKey: null,
markCommentAsVisible: jest.fn(),
markAllCommentsAsRead: jest.fn(),
});
(NewCommentIndicator as jest.Mock).mockImplementation(() => (
<div data-testid="new-comment-indicator" />
));
});
beforeAll(() => {
class IntersectionObserverMock {
callback: IntersectionObserverCallback;
constructor(callback: IntersectionObserverCallback) {
this.callback = callback;
}
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
configurable: true,
value: IntersectionObserverMock,
});
Object.defineProperty(global, "IntersectionObserver", {
writable: true,
configurable: true,
value: IntersectionObserverMock,
});
});
it("renders title and description copy", () => {
const { result } = renderHook(() => useGetClarificationCopy());
renderWithTheme(<ClarificationContainer {...defaultProps} />);
expect(screen.getByText(result.current.title)).toBeInTheDocument();
expect(screen.getByText(result.current.description)).toBeInTheDocument();
});
it("does not display description if commenting is not allowed (terminal status)", () => {
const { result } = renderHook(() => useGetClarificationCopy());
renderWithTheme(
<ClarificationContainer {...defaultProps} status="approved" />
);
expect(screen.getByText(result.current.title)).toBeInTheDocument();
expect(
screen.queryByText(result.current.description)
).not.toBeInTheDocument();
});
it("renders ClarificationComment components for each message", () => {
renderWithTheme(
<ClarificationContainer
{...defaultProps}
clarificationFlowMessages={mockMessages}
/>
);
const commentListArea = screen.getByTestId("comment-list-area");
mockMessages.forEach(({ message }) => {
expect(within(commentListArea).getByText(message)).toBeInTheDocument();
});
});
it("renders NewCommentIndicator above the first unread comment if unread exists and not terminal", () => {
const firstUnreadCommentKey = buildCommentEntityKey(
approvalRequestId,
mockMessages[0]!
);
(useGetUnreadComments as jest.Mock).mockReturnValue({
unreadComments: [mockMessages[0]],
firstUnreadCommentKey,
markCommentAsVisible: jest.fn(),
markAllCommentsAsRead: jest.fn(),
});
renderWithTheme(
<ClarificationContainer
{...defaultProps}
clarificationFlowMessages={mockMessages}
/>
);
expect(screen.getByTestId("new-comment-indicator")).toBeInTheDocument();
});
it("does not render NewCommentIndicator if status is terminal", () => {
const firstUnreadCommentKey = buildCommentEntityKey(
approvalRequestId,
mockMessages[0]!
);
(useGetUnreadComments as jest.Mock).mockReturnValue({
unreadComments: [mockMessages[0]],
firstUnreadCommentKey,
markCommentAsVisible: jest.fn(),
markAllCommentsAsRead: jest.fn(),
});
renderWithTheme(
<ClarificationContainer
{...defaultProps}
status="approved"
clarificationFlowMessages={mockMessages}
/>
);
expect(
screen.queryByTestId("new-comment-indicator")
).not.toBeInTheDocument();
});
it("renders ClarificationCommentForm if canComment is true", () => {
const messages = Array.from({ length: MAX_COMMENTS - 1 }, (_, i) =>
createClarificationFlowMessage(i + 1)
);
renderWithTheme(
<ClarificationContainer
{...defaultProps}
clarificationFlowMessages={messages}
/>
);
expect(screen.getByRole("textbox")).toBeInTheDocument();
});
it("calls markAllCommentsAsRead on window beforeunload event", () => {
const markAllCommentsAsReadMock = jest.fn();
(useGetUnreadComments as jest.Mock).mockReturnValue({
unreadComments: [],
firstUnreadCommentKey: null,
markCommentAsVisible: jest.fn(),
markAllCommentsAsRead: markAllCommentsAsReadMock,
});
renderWithTheme(<ClarificationContainer {...defaultProps} />);
fireEvent(window, new Event("beforeunload"));
expect(markAllCommentsAsReadMock).toHaveBeenCalled();
});
it("cleans up beforeunload listener on unmount", () => {
const addEventListenerSpy = jest.spyOn(window, "addEventListener");
const removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
const { unmount } = renderWithTheme(
<ClarificationContainer {...defaultProps} />
);
expect(addEventListenerSpy).toHaveBeenCalledWith(
"beforeunload",
expect.any(Function)
);
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith(
"beforeunload",
expect.any(Function)
);
});
});

View File

@@ -0,0 +1,102 @@
import { fireEvent, screen } from "@testing-library/react";
import CommentLimitAlert from "../CommentLimitAlert";
import { MAX_COMMENTS } from "../constants";
import { renderWithTheme } from "../../../../testHelpers";
beforeEach(() => {
localStorage.clear();
});
describe("CommentLimitAlert component", () => {
const defaultProps = {
approvalRequestId: "123",
currentUserId: 456,
};
it("renders nothing if isTerminalStatus is true", () => {
renderWithTheme(
<CommentLimitAlert
{...defaultProps}
commentCount={MAX_COMMENTS}
isTerminalStatus
/>
);
expect(screen.queryByRole("alert")).toBeNull();
});
it("renders max comments alert when commentCount >= MAX_COMMENTS", () => {
renderWithTheme(
<CommentLimitAlert
{...defaultProps}
commentCount={MAX_COMMENTS}
isTerminalStatus={false}
/>
);
expect(screen.getByText("Comment limit reached")).toBeInTheDocument();
expect(
screen.getByText(
"You can't add more comments, approvers can still approve or deny."
)
).toBeInTheDocument();
});
it("renders near limit alert when commentCount is near max and alert is visible", () => {
renderWithTheme(
<CommentLimitAlert
{...defaultProps}
commentCount={MAX_COMMENTS - 1}
isTerminalStatus={false}
/>
);
expect(
screen.getByText("Comment limit nearly reached")
).toBeInTheDocument();
expect(
screen.getByText(
"This request has 39 of 40 comments available. You have 1 remaining."
)
).toBeInTheDocument();
});
it("does not render near limit alert if previously dismissed", () => {
const alertDismissalKey = `nearLimitAlertDismissed_${defaultProps.currentUserId}_${defaultProps.approvalRequestId}`;
localStorage.setItem(alertDismissalKey, "true");
renderWithTheme(
<CommentLimitAlert
{...defaultProps}
commentCount={MAX_COMMENTS - 1}
isTerminalStatus={false}
/>
);
expect(screen.queryByText("Comment limit nearly reached")).toBeNull();
});
it("dismiss near limit alert on close button click", () => {
renderWithTheme(
<CommentLimitAlert
{...defaultProps}
commentCount={MAX_COMMENTS - 1}
isTerminalStatus={false}
/>
);
const closeButton = screen.getByRole("button", {
name: "Close alert",
});
expect(closeButton).toBeInTheDocument();
fireEvent.click(closeButton);
expect(screen.queryByText("Comment limit nearly reached")).toBeNull();
const alertDismissalKey = `nearLimitAlertDismissed_${defaultProps.currentUserId}_${defaultProps.approvalRequestId}`;
expect(localStorage.getItem(alertDismissalKey)).toBe("true");
});
});

View File

@@ -0,0 +1,15 @@
import { screen } from "@testing-library/react";
import NewCommentIndicator from "../NewCommentIndicator";
import { renderWithTheme } from "../../../../testHelpers";
describe("NewCommentIndicator", () => {
it("renders singular new comment text when unreadCount is 1", () => {
renderWithTheme(<NewCommentIndicator unreadCount={1} />);
expect(screen.getByText("New comment")).toBeInTheDocument();
});
it("renders plural new comments text when unreadCount is greater than 1", () => {
renderWithTheme(<NewCommentIndicator unreadCount={5} />);
expect(screen.getByText("New comments")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,8 @@
import type { ApprovalClarificationFlowMessage } from "../../../types";
export const buildCommentEntityKey = (
approvalRequestId: string,
comment: ApprovalClarificationFlowMessage
) => {
return `zenGuide:approvalRequest:${approvalRequestId}:comment:${comment.id}`;
};

View File

@@ -0,0 +1,6 @@
export const APPROVAL_REQUEST_STATES = {
ACTIVE: "active",
APPROVED: "approved",
REJECTED: "rejected",
WITHDRAWN: "withdrawn",
} as const;

View File

@@ -0,0 +1,48 @@
import { useEffect, useState } from "react";
import type { ApprovalRequest } from "../types";
export function useApprovalRequest(
approvalWorkflowInstanceId: string,
approvalRequestId: string
): {
approvalRequest: ApprovalRequest | undefined;
errorFetchingApprovalRequest: unknown;
isLoading: boolean;
setApprovalRequest: (approvalRequest: ApprovalRequest) => void;
} {
const [approvalRequest, setApprovalRequest] = useState<
ApprovalRequest | undefined
>();
const [error, setError] = useState<unknown>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchApprovalRequest = async () => {
setIsLoading(true);
try {
const response = await fetch(
`/api/v2/approval_workflow_instances/${approvalWorkflowInstanceId}/approval_requests/${approvalRequestId}`
);
if (response.ok) {
const data = await response.json();
setApprovalRequest(data.approval_request);
} else {
throw new Error("Error fetching approval request");
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchApprovalRequest();
}, [approvalRequestId, approvalWorkflowInstanceId]);
return {
approvalRequest,
errorFetchingApprovalRequest: error,
isLoading,
setApprovalRequest,
};
}

View File

@@ -0,0 +1,74 @@
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import type {
ApprovalRequestDropdownStatus,
SearchApprovalRequest,
} from "../types";
export function useSearchApprovalRequests(): {
approvalRequests: SearchApprovalRequest[];
errorFetchingApprovalRequests: unknown;
approvalRequestStatus: ApprovalRequestDropdownStatus;
setApprovalRequestStatus: Dispatch<
SetStateAction<ApprovalRequestDropdownStatus>
>;
isLoading: boolean;
} {
const [approvalRequests, setApprovalRequests] = useState<
SearchApprovalRequest[]
>([]);
const [error, setError] = useState<unknown>(null);
const [isLoading, setIsLoading] = useState(false);
const [approvalRequestStatus, setApprovalRequestStatus] =
useState<ApprovalRequestDropdownStatus>("any");
useEffect(() => {
const fetchApprovalRequests = async () => {
setIsLoading(true);
try {
const currentUserRequest = await fetch("/api/v2/users/me.json");
if (!currentUserRequest.ok) {
throw new Error("Error fetching current user data");
}
const currentUser = await currentUserRequest.json();
// TODO: can be any ULID, the API was implemented this way for future proofing, we will likely need to update the route in the UI to match
const approvalWorkflowInstanceId = "01JJQFNX5ADZ6PRQCFWRDNKZRD";
const response = await fetch(
`/api/v2/approval_workflow_instances/${approvalWorkflowInstanceId}/approval_requests/search${
approvalRequestStatus === "any"
? ""
: `?status=${approvalRequestStatus}`
}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": currentUser.user.authenticity_token,
},
}
);
if (response.ok) {
const data = await response.json();
setApprovalRequests(data.approval_requests);
} else {
throw new Error("Error fetching approval requests");
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchApprovalRequests();
}, [approvalRequestStatus]);
return {
approvalRequests,
errorFetchingApprovalRequests: error,
approvalRequestStatus,
setApprovalRequestStatus,
isLoading: isLoading,
};
}

View File

@@ -0,0 +1,51 @@
import { useState, useCallback, useEffect } from "react";
interface UseUserViewedApprovalStatusParams {
approvalRequestId: string | undefined;
currentUserId: number;
}
/**
* Hook that tracks whether a user has viewed a specific approval request before, using localStorage.
*/
export function useUserViewedApprovalStatus({
approvalRequestId,
currentUserId,
}: UseUserViewedApprovalStatusParams) {
const isValid = approvalRequestId !== undefined;
const storage = window.localStorage;
const storageKey = `userViewedApproval:${currentUserId}:${approvalRequestId}`;
const getViewedStatus = useCallback((): boolean => {
if (!isValid) return false;
try {
return storage.getItem(storageKey) === "true";
} catch {
return false;
}
}, [isValid, storage, storageKey]);
const [hasUserViewedBefore, setHasUserViewedBefore] = useState<boolean>(
() => {
return getViewedStatus();
}
);
const markUserViewed = useCallback(() => {
if (!isValid) return;
try {
storage.setItem(storageKey, "true");
setHasUserViewedBefore(true);
} catch {
// ignore errors
}
}, [isValid, storage, storageKey]);
useEffect(() => {
if (!isValid) return;
setHasUserViewedBefore(getViewedStatus());
}, [approvalRequestId, currentUserId, getViewedStatus, isValid]);
return { hasUserViewedBefore, markUserViewed };
}

View File

@@ -0,0 +1,2 @@
export { renderApprovalRequestList } from "./renderApprovalRequestList";
export { renderApprovalRequest } from "./renderApprovalRequest";

View File

@@ -0,0 +1,35 @@
import { render } from "react-dom";
import ApprovalRequestPage from "./ApprovalRequestPage";
import type { ApprovalRequestPageProps } from "./ApprovalRequestPage";
import {
createTheme,
ThemeProviders,
initI18next,
loadTranslations,
} from "../shared";
import type { Settings } from "../shared";
import { ErrorBoundary } from "../shared/error-boundary/ErrorBoundary";
export async function renderApprovalRequest(
container: HTMLElement,
settings: Settings,
props: ApprovalRequestPageProps,
helpCenterPath: string
) {
const { baseLocale } = props;
initI18next(baseLocale);
await loadTranslations(baseLocale, [
() => import(`./translations/locales/${baseLocale}.json`),
() => import(`../shared/translations/locales/${baseLocale}.json`),
]);
render(
<ThemeProviders theme={createTheme(settings)}>
<ErrorBoundary helpCenterPath={helpCenterPath}>
<ApprovalRequestPage {...props} helpCenterPath={helpCenterPath} />
</ErrorBoundary>
</ThemeProviders>,
container
);
}

View File

@@ -0,0 +1,34 @@
import { render } from "react-dom";
import ApprovalRequestListPage from "./ApprovalRequestListPage";
import type { ApprovalRequestListPageProps } from "./ApprovalRequestListPage";
import {
createTheme,
ThemeProviders,
initI18next,
loadTranslations,
} from "../shared";
import type { Settings } from "../shared";
import { ErrorBoundary } from "../shared/error-boundary/ErrorBoundary";
export async function renderApprovalRequestList(
container: HTMLElement,
settings: Settings,
props: ApprovalRequestListPageProps,
helpCenterPath: string
) {
const { baseLocale } = props;
initI18next(baseLocale);
await loadTranslations(baseLocale, [
() => import(`./translations/locales/${baseLocale}.json`),
() => import(`../shared/translations/locales/${baseLocale}.json`),
]);
render(
<ThemeProviders theme={createTheme(settings)}>
<ErrorBoundary helpCenterPath={helpCenterPath}>
<ApprovalRequestListPage {...props} helpCenterPath={helpCenterPath} />
</ErrorBoundary>
</ThemeProviders>,
container
);
}

View File

@@ -0,0 +1,35 @@
export type ApprovalDecision = "approved" | "rejected";
export async function submitApprovalDecision(
approvalWorkflowInstanceId: string,
approvalRequestId: string,
decision: ApprovalDecision,
decisionNote: string
) {
try {
const currentUserRequest = await fetch("/api/v2/users/me.json");
if (!currentUserRequest.ok) {
throw new Error("Error fetching current user data");
}
const currentUser = await currentUserRequest.json();
const response = await fetch(
`/api/v2/approval_workflow_instances/${approvalWorkflowInstanceId}/approval_requests/${approvalRequestId}/decision`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": currentUser.user.authenticity_token,
},
body: JSON.stringify({
status: decision,
notes: decisionNote,
}),
}
);
return response;
} catch (error) {
console.error("Error submitting approval decision:", error);
throw error;
}
}

View File

@@ -0,0 +1,7 @@
import type { ReactElement } from "react";
import { ThemeProvider, DEFAULT_THEME } from "@zendeskgarden/react-theming";
import { render } from "@testing-library/react";
export const renderWithTheme = (ui: ReactElement) => {
return render(<ThemeProvider theme={DEFAULT_THEME}>{ui}</ThemeProvider>);
};

View File

@@ -0,0 +1,319 @@
#
# This file is used for the internal Zendesk translation system, and it is generated from the extract-strings.mjs script.
# It contains the English strings to be translated, which are used for generating the JSON files containing the translation strings
# for each language.
#
# If you are building your own theme, you can remove this file and just load the translation JSON files, or provide
# your translations with a different method.
#
title: "Copenhagen Theme Approval requests"
packages:
- "approval-requests"
parts:
- translation:
key: "approval-requests.request.ticket-details.header"
title: "Header for the ticket details section displayed on the individual approval request page"
screenshot: "https://drive.google.com/file/d/1-mngvtPAewxxavZdD5s9XZrIUgtGW4Vj/view?usp=drive_link"
value: "Ticket details"
- translation:
key: "approval-requests.request.ticket-details.requester"
title: "Label for the person who submitted the ticket associated with the approval request"
screenshot: "https://drive.google.com/file/d/14qPWuPcsMFDC6J_HhOBbNlI9hBeLMKA-/view?usp=sharing"
value: "Requester"
- translation:
key: "approval-requests.request.ticket-details.id"
title: "Label for the unique identifier of the ticket associated with the approval request"
screenshot: "https://drive.google.com/file/d/1IJdg-vXQAUtcgS-JWWwgVOyRg5IO8Cey/view?usp=drive_link"
value: "ID"
- translation:
key: "approval-requests.request.ticket-details.priority"
title: "Label for the priority of the ticket associated with the approval request"
screenshot: "https://drive.google.com/file/d/1rB8uGfpnaDXGATBlsNyRdbntZKvS4BeW/view?usp=sharing"
value: "Priority"
- translation:
key: "approval-requests.request.ticket-details.priority_low"
title: "Low priority level for a ticket associated with an approval request"
screenshot: "https://drive.google.com/file/d/1-RB1gY755d5Ffr_1rirtajAk1wxnmC_L/view?usp=sharing"
value: "Low"
- translation:
key: "approval-requests.request.ticket-details.priority_normal"
title: "Normal priority level for a ticket associated with an approval request"
screenshot: "https://drive.google.com/file/d/1WYuPkB_Ju-CcfjX4E6R3cY8plXMhfzu5/view?usp=sharing"
value: "Normal"
- translation:
key: "approval-requests.request.ticket-details.priority_high"
title: "High priority level for a ticket associated with an approval request"
screenshot: "https://drive.google.com/file/d/17M5eebl-XR_Z8mrF5VNwkRSOCH8qbPqQ/view?usp=sharing"
value: "High"
- translation:
key: "approval-requests.request.ticket-details.priority_urgent"
title: "Urgent priority level for a ticket associated with an approval request"
screenshot: "https://drive.google.com/file/d/1PsUJkvvePPAL7GuEXLiV6AeRSleu9lib/view?usp=sharing"
value: "Urgent"
- translation:
key: "approval-requests.request.ticket-details.checkbox-value.yes"
title: "Value representing true for a checkbox custom field"
screenshot: "https://drive.google.com/file/d/1Vk8x7k2DzF7t9DT5Q-KfmEbCZRmFOBeE/view?usp=drive_link"
value: "Yes"
- translation:
key: "approval-requests.request.ticket-details.checkbox-value.no"
title: "Value representing false for a checkbox custom field"
screenshot: "https://drive.google.com/file/d/1AiuLQXnW6qELTJtIFQX0JaBCA7zViMXA/view?usp=drive_link"
value: "No"
- translation:
key: "approval-requests.request.approval-request-details.header"
title: "Header for the approval request section displayed on the individual approval request page"
screenshot: "https://drive.google.com/file/d/1fteldN_Z4yTgd7UPzQi0xYv8TTPJxX9c/view?usp=drive_link"
value: "Approval request details"
- translation:
key: "approval-requests.request.approval-request-details.sent-by"
title: "Label for the person who submitted the approval request - Subject: 'approval request'"
screenshot: "https://drive.google.com/file/d/1-RFlHvEdegvChs6TuVTXxx6ubeSab-hc/view?usp=drive_link"
value: "Sent by"
- translation:
key: "approval-requests.request.approval-request-details.sent-on"
title: "Label for the date that the approval request was submitted - Subject: 'approval request'"
screenshot: "https://drive.google.com/file/d/1Kmy-XgNfCE5dgh-oipsc1P_sZ3gmvjD6/view?usp=drive_link"
value: "Sent on"
- translation:
key: "approval-requests.request.approval-request-details.approver"
title: "Label for the individual assigned to review and approve the request"
screenshot: "https://drive.google.com/file/d/1VnGlaQE9sSsKqwY3iKZn-AiJK8RnfUL8/view?usp=drive_link"
value: "Approver"
- translation:
key: "approval-requests.request.approval-request-details.status"
title: "Label for the current status of the approval request"
screenshot: "https://drive.google.com/file/d/11G6qZpoQoMzyEUxncBo4RovT93thSuXx/view?usp=drive_link"
value: "Status"
- translation:
key: "approval-requests.request.approval-request-details.comment"
title: "Noun - Label for the decision comment on an approval request"
screenshot: "https://drive.google.com/file/d/1j9IzjnAa1AVXzWq6r4x4QcaQN-Y64lM5/view?usp=drive_link"
value: "Comment"
- translation:
key: "approval-requests.request.approval-request-details.decided"
title: "Label for the decision date on an approval request - Subject: 'approval request'"
screenshot: "https://drive.google.com/file/d/1xpM_3Rt3hCD8aE-U7mEbL0Sh5dsC0ScN/view?usp=drive_link"
value: "Decided"
- translation:
key: "approval-requests.request.approval-request-details.withdrawn-on"
title: "Label for the withdrawn date on an approval request - Subject: 'approval request'"
screenshot: "https://drive.google.com/file/d/1hkgy-71WFkLQnZ0PmEHFaWhfnuguiUmv/view?usp=drive_link"
value: "Withdrawn on"
- translation:
key: "approval-requests.request.approval-request-details.previous-decision"
title: "Label in approval request details component"
screenshot: "https://drive.google.com/file/d/1R55CIlKhowOKZ9J22ChB81qiMz2NifYO/view?usp=drive_link"
value: "Previous decision"
- translation:
key: "approval-requests.request.approver-actions.approve-request"
title: "Label for the approve request button to begin approving an approval request"
screenshot: "https://drive.google.com/file/d/1uWDMNa7cfF2EdD5HpDxcTVHTa6M07v81/view?usp=drive_link"
value: "Approve request"
- translation:
key: "approval-requests.request.approver-actions.deny-request"
title: "Label for the deny request button to begin denying an approval request"
screenshot: "https://drive.google.com/file/d/1sEO2wxGJU6Jz1ZClBvkYDTXCXT9yqsGZ/view?usp=drive_link"
value: "Deny request"
- translation:
key: "approval-requests.request.approver-actions.denial-reason-label"
title: "Label for the input box where the reason for denying an approval request is provided"
screenshot: "https://drive.google.com/file/d/1WZx7DbwkudNbNYMIEwF56npryQGIpsG7/view?usp=drive_link"
value: "Reason for denial* (Required)"
- translation:
key: "approval-requests.request.approver-actions.denial-reason-validation"
title: "Validation message shown when attempting to deny an approval request without a reason for denial"
screenshot: "https://drive.google.com/file/d/1-wo8BGxMuV6bHD7mjlX8Lm3OTc_9RyFz/view?usp=drive_link"
value: "Enter a reason for denial"
- translation:
key: "approval-requests.request.approver-actions.additional-note-label"
title: "Label for the input box where the additional note when approving an approval request is provided"
screenshot: "https://drive.google.com/file/d/1gacesWd8MkyIY6UXvQSatv2ok67ZhbKk/view?usp=drive_link"
value: "Additional note"
- translation:
key: "approval-requests.request.approver-actions.submit-approval"
title: "Label for the submit approval button when approving an approval request"
screenshot: "https://drive.google.com/file/d/12C2F8zoCD_nMU-jSeu4kaMWwTeJNE3HJ/view?usp=drive_link"
value: "Submit approval"
- translation:
key: "approval-requests.request.approver-actions.submit-denial"
title: "Label for the submit denial button when denying an approval request"
screenshot: "https://drive.google.com/file/d/153Idb3CgJ_eFfWxLE4esAdB0DceOH8gj/view?usp=drive_link"
value: "Submit denial"
- translation:
key: "approval-requests.request.approver-actions.cancel"
title: "Label for the cancel button when approving or denying an approval request"
screenshot: "https://drive.google.com/file/d/1TjjF7CrDr-VYbZ-rpGs0pLCersevq9uL/view?usp=drive_link"
value: "Cancel"
- translation:
key: "approval-requests.request.notification.denial-submitted"
title: "Notification message shown when the denial of an approval request was submitted"
screenshot: "https://drive.google.com/file/d/1_Af2Xi2l6kcKI9QmFFQxH3SvI_Eaarg5/view?usp=drive_link"
value: "Denial submitted"
- translation:
key: "approval-requests.request.notification.approval-submitted"
title: "Notification message shown when the approval of an approval request was submitted"
screenshot: "https://drive.google.com/file/d/16BOEFXrbM29me3KdGgROerXXO5ns-7oh/view?usp=drive_link"
value: "Approval submitted"
- translation:
key: "approval-requests.list.header"
title: "Header for the approval requests list page"
screenshot: "https://drive.google.com/file/d/1MUj23YI3kUT-Udmh1LA0rt85-q078SMj/view?usp=drive_link"
value: "Approval requests"
- translation:
key: "approval-requests.list.search-placeholder"
title: "Placeholder for the search dropdown on the approval requests list page"
screenshot: "https://drive.google.com/file/d/10Mdnw0YkIq3w4CjdDOSD9efAAxDapzYr/view?usp=drive_link"
value: "Search approval requests"
- translation:
key: "approval-requests.list.no-requests"
title: "Text to convey that no approval requests exist on the approval requests list page."
screenshot: "https://drive.google.com/file/d/1rrZ1MZKAsY7mNo89McYaZNwEB5pDjKtk/view?usp=sharing"
value: "No approval requests found."
- translation:
key: "approval-requests.list.status-dropdown.label"
title: "Label for the status dropdown on the approval requests list page"
screenshot: "https://drive.google.com/file/d/18_4fS0yROAMC1yVdDYLfYvjKWeUdWCDH/view?usp=drive_link"
value: "Status:"
obsolete: "2025-05-24"
- translation:
key: "approval-requests.list.status-dropdown.label_v2"
title: "Label for the status dropdown on the approval requests list page"
screenshot: "https://drive.google.com/file/d/1T7BGzFgCWC74RIoUBBZ8tZzAvcksIw1r/view?usp=drive_link"
value: "Status"
- translation:
key: "approval-requests.list.status-dropdown.any"
title: "Label for any dropdown option within the status dropdown on the approval requests list page - Subject: 'status'"
screenshot: "https://drive.google.com/file/d/1dIURRxQu-EkQD9f1U0gKj_4mIBqBSRz3/view?usp=drive_link"
value: "Any"
- translation:
key: "approval-requests.list.table.subject"
title: "Header cell label for the subject column of the approval requests list table"
screenshot: "https://drive.google.com/file/d/1AfMOHvrRQoTODXWor5gtn7wn8AFI_5n-/view?usp=drive_link"
value: "Subject"
- translation:
key: "approval-requests.list.table.requester"
title: "Header cell label for the requester column of the approval requests list table"
screenshot: "https://drive.google.com/file/d/1tUTbmtSZCYxxDVbpQtZ6C50QDYlJU88I/view?usp=drive_link"
value: "Requester"
- translation:
key: "approval-requests.list.table.sent-by"
title: "Header cell label for the sent by column of the approval requests list table - Subject: 'approval request'"
screenshot: "https://drive.google.com/file/d/1k49KpSKk1OT4NYfeZqdb7WZLEwJZndpq/view?usp=drive_link"
value: "Sent by"
- translation:
key: "approval-requests.list.table.sent-on"
title: "Header cell label for the sent on column of the approval requests list table - Subject: 'approval request'"
screenshot: "https://drive.google.com/file/d/1i2fKqUDlWbNnYRZdxp041dAPhMsFznXO/view?usp=drive_link"
value: "Sent on"
- translation:
key: "approval-requests.list.table.approval-status"
title: "Header cell label for the approval status column of the approval requests list table"
screenshot: "https://drive.google.com/file/d/1ltG2DXbLbR1-6CaK_su1pbKQeN7kVMMV/view?usp=drive_link"
value: "Approval status"
- translation:
key: "approval-requests.status.decision-pending"
title: "Status label for the decision pending status of an approval request - Subject: 'status'"
screenshot: "https://drive.google.com/file/d/1wMA0ZAm8f_Mw0wgj7UKl7wkBU8XGH_ox/view?usp=drive_link"
value: "Decision pending"
- translation:
key: "approval-requests.status.info-needed"
title: "Status label for the info needed status of an approval request - Subject: 'status'"
screenshot: "https://drive.google.com/file/d/11Isf_F0qGOFh6Dv5RnR44ta5yJxX9n_4/view?usp=drive_link"
value: "Info needed"
- translation:
key: "approval-requests.status.approved"
title: "Status label for the approved status of an approval request - Subject: 'status'"
screenshot: "https://drive.google.com/file/d/1Xc8uehxpLHQR2Pjxfyo-wbvooPwe8-WX/view?usp=drive_link"
value: "Approved"
- translation:
key: "approval-requests.status.denied"
title: "Status label for the denied status of an approval request - Subject: 'status'"
screenshot: "https://drive.google.com/file/d/13qjrnfFVk61r5QMxd4CL_B8RY4cYzO1r/view?usp=drive_link"
value: "Denied"
- translation:
key: "approval-requests.status.withdrawn"
title: "Status label for the withdrawn status of an approval request - Subject: 'status'"
screenshot: "https://drive.google.com/file/d/1iOTsHEB5xae4LGZbjr4ibtdSDf0h9BOE/view?usp=drive_link"
value: "Withdrawn"
- translation:
key: "txt.approval_requests.clarification.title"
title: "Title of the comments section"
value: "Comments"
screenshot: "https://drive.google.com/file/d/1_71h6mJIjYHEDXPYLZ6vjZt_y3Nx9ZDD/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.description"
title: "Description of the comments section"
value: "Add notes or ask for additional information about this request"
screenshot: "https://drive.google.com/file/d/1_71h6mJIjYHEDXPYLZ6vjZt_y3Nx9ZDD/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.comment_form_aria_label"
title: "[A11Y] Hidden accessibility label for the comment form in the single approval request sidebar panel"
value: "Enter a comment to ask for additional information about this approval request"
screenshot: "https://drive.google.com/file/d/194B7-DmEvZJOB5X6QyNXNKGEyLsbjOdw/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.submit_button"
title: "Submit comment button label"
value: "Send"
screenshot: "https://drive.google.com/file/d/1_71h6mJIjYHEDXPYLZ6vjZt_y3Nx9ZDD/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.cancel_button"
title: "Cancel comment button label"
value: "Cancel"
screenshot: "https://drive.google.com/file/d/1_71h6mJIjYHEDXPYLZ6vjZt_y3Nx9ZDD/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.characters_remaining.zero"
title: "Warning validation message informing the user that there are zero remaining characters before they reach the limit."
value: "{{numCharacters}} characters remaining"
screenshot: "https://drive.google.com/file/d/1MSVyUqRUqCu9vAYfRzaHgZw82vvF6qNv/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.characters_remaining.one"
title: "Warning validation message informing the user that there is one remaining character before they reach the limit."
value: "{{numCharacters}} character remaining"
screenshot: "https://drive.google.com/file/d/17nQCx_XH2IMJtXVomjPYTLRI1i-WYI1W/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.characters_remaining.two"
title: "Warning validation message informing the user that there are two remaining characters before they reach the limit."
value: "{{numCharacters}} characters remaining"
screenshot: "https://drive.google.com/file/d/1atQ7bIrRvXXUg8Wl3il26l1UC0CiDA6u/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.characters_remaining.few"
title: "Warning validation message informing the user of the number of characters left before the limit, when the few plural form applies"
value: "{{numCharacters}} characters remaining"
screenshot: "https://drive.google.com/file/d/1atQ7bIrRvXXUg8Wl3il26l1UC0CiDA6u/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.characters_remaining.many"
title: "Warning validation message informing the user of the number of characters left before the limit, when the many plural form applies"
value: "{{numCharacters}} characters remaining"
screenshot: "https://drive.google.com/file/d/1atQ7bIrRvXXUg8Wl3il26l1UC0CiDA6u/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.characters_remaining.other"
title: "Warning validation message informing the user of the number of characters left before the limit, when the default plural form applies"
value: "{{numCharacters}} characters remaining"
screenshot: "https://drive.google.com/file/d/1atQ7bIrRvXXUg8Wl3il26l1UC0CiDA6u/view?usp=sharing"
- translation:
key: "txt.approval_requests.validation.empty_comment_error"
title: "Error validation message informing the user that the comment field is empty"
value: "Enter a comment"
screenshot: "https://drive.google.com/file/d/1EjO40n6Vg3H5gUpNqp3TKUXSdQ0Hyg6S/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.new_comment_indicator"
title: "This is the text that appears when a new comment is added to the approval request clarification section"
value: "New comment"
screenshot: "https://drive.google.com/file/d/1HngajEqfo_Y6Q5dU8CzCSfgmS1nLGeEQ/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.new_comments_indicator"
title: "This is the text that appears when multiple new comments are added to the approval request clarification section"
value: "New comments"
screenshot: "https://drive.google.com/file/d/1HusOTEN6w5Pc820fsdGhKtov0YGA-OZ9/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.max_comment_alert_title"
title: "This is the title for an alert notification that is shown when 40 comments has been added to the approval request clarification section"
value: "Comment limit reached"
screenshot: "https://drive.google.com/file/d/1zJ4YRvuaKSbgUhvXW7urZUmVYo-x0f4M/view?usp=sharing"
- translation:
key: "txt.approval_requests.clarification.max_comment_alert_message"
title: "This is the message for an alert notification that is shown when 40 comments has been added to the approval request clarification section"
value: "You can't add more comments, approvers can still approve or deny."
screenshot: "https://drive.google.com/file/d/1zJ4YRvuaKSbgUhvXW7urZUmVYo-x0f4M/view?usp=sharing"

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭṡ龍ผู้]",
"approval-requests.list.no-requests": "[ผู้龍Ṅṓṓ ααṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭṡ ϝṓṓṵṵṇḍ.龍ผู้]",
"approval-requests.list.search-placeholder": "[ผู้龍Ṣḛḛααṛͼḥ ααṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭṡ龍ผู้]",
"approval-requests.list.status-dropdown.any": "[ผู้龍ḀḀṇẏẏ龍ผู้]",
"approval-requests.list.status-dropdown.label_v2": "[ผู้龍Ṣṭααṭṵṵṡ龍ผู้]",
"approval-requests.list.table.approval-status": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṡṭααṭṵṵṡ龍ผู้]",
"approval-requests.list.table.requester": "[ผู้龍Ṛḛḛʠṵṵḛḛṡṭḛḛṛ龍ผู้]",
"approval-requests.list.table.sent-by": "[ผู้龍Ṣḛḛṇṭ ḅẏẏ龍ผู้]",
"approval-requests.list.table.sent-on": "[ผู้龍Ṣḛḛṇṭ ṓṓṇ龍ผู้]",
"approval-requests.list.table.subject": "[ผู้龍Ṣṵṵḅĵḛḛͼṭ龍ผู้]",
"approval-requests.request.approval-request-details.approver": "[ผู้龍ḀḀṗṗṛṓṓṽḛḛṛ龍ผู้]",
"approval-requests.request.approval-request-details.comment": "[ผู้龍Ḉṓṓṃṃḛḛṇṭ龍ผู้]",
"approval-requests.request.approval-request-details.decided": "[ผู้龍Ḍḛḛͼḭḭḍḛḛḍ龍ผู้]",
"approval-requests.request.approval-request-details.header": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭ ḍḛḛṭααḭḭḽṡ龍ผู้]",
"approval-requests.request.approval-request-details.previous-decision": "[ผู้龍Ṕṛḛḛṽḭḭṓṓṵṵṡ ḍḛḛͼḭḭṡḭḭṓṓṇ龍ผู้]",
"approval-requests.request.approval-request-details.sent-by": "[ผู้龍Ṣḛḛṇṭ ḅẏẏ龍ผู้]",
"approval-requests.request.approval-request-details.sent-on": "[ผู้龍Ṣḛḛṇṭ ṓṓṇ龍ผู้]",
"approval-requests.request.approval-request-details.status": "[ผู้龍Ṣṭααṭṵṵṡ龍ผู้]",
"approval-requests.request.approval-request-details.withdrawn-on": "[ผู้龍Ŵḭḭṭḥḍṛααẁṇ ṓṓṇ龍ผู้]",
"approval-requests.request.approver-actions.additional-note-label": "[ผู้龍ḀḀḍḍḭḭṭḭḭṓṓṇααḽ ṇṓṓṭḛḛ龍ผู้]",
"approval-requests.request.approver-actions.approve-request": "[ผู้龍ḀḀṗṗṛṓṓṽḛḛ ṛḛḛʠṵṵḛḛṡṭ龍ผู้]",
"approval-requests.request.approver-actions.cancel": "[ผู้龍Ḉααṇͼḛḛḽ龍ผู้]",
"approval-requests.request.approver-actions.denial-reason-label": "[ผู้龍Ṛḛḛααṡṓṓṇ ϝṓṓṛ ḍḛḛṇḭḭααḽ* (Ṛḛḛʠṵṵḭḭṛḛḛḍ)龍ผู้]",
"approval-requests.request.approver-actions.denial-reason-validation": "[ผู้龍ḚḚṇṭḛḛṛ αα ṛḛḛααṡṓṓṇ ϝṓṓṛ ḍḛḛṇḭḭααḽ龍ผู้]",
"approval-requests.request.approver-actions.deny-request": "[ผู้龍Ḍḛḛṇẏẏ ṛḛḛʠṵṵḛḛṡṭ龍ผู้]",
"approval-requests.request.approver-actions.submit-approval": "[ผู้龍Ṣṵṵḅṃḭḭṭ ααṗṗṛṓṓṽααḽ龍ผู้]",
"approval-requests.request.approver-actions.submit-denial": "[ผู้龍Ṣṵṵḅṃḭḭṭ ḍḛḛṇḭḭααḽ龍ผู้]",
"approval-requests.request.notification.approval-submitted": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṡṵṵḅṃḭḭṭṭḛḛḍ龍ผู้]",
"approval-requests.request.notification.denial-submitted": "[ผู้龍Ḍḛḛṇḭḭααḽ ṡṵṵḅṃḭḭṭṭḛḛḍ龍ผู้]",
"approval-requests.request.ticket-details.checkbox-value.no": "[ผู้龍Ṅṓṓ龍ผู้]",
"approval-requests.request.ticket-details.checkbox-value.yes": "[ผู้龍ŶŶḛḛṡ龍ผู้]",
"approval-requests.request.ticket-details.header": "[ผู้龍Ṫḭḭͼḳḛḛṭ ḍḛḛṭααḭḭḽṡ龍ผู้]",
"approval-requests.request.ticket-details.id": "[ผู้龍ḬḬḌ龍ผู้]",
"approval-requests.request.ticket-details.priority": "[ผู้龍Ṕṛḭḭṓṓṛḭḭṭẏẏ龍ผู้]",
"approval-requests.request.ticket-details.priority_high": "[ผู้龍Ḥḭḭḡḥ龍ผู้]",
"approval-requests.request.ticket-details.priority_low": "[ผู้龍Ḻṓṓẁ龍ผู้]",
"approval-requests.request.ticket-details.priority_normal": "[ผู้龍Ṅṓṓṛṃααḽ龍ผู้]",
"approval-requests.request.ticket-details.priority_urgent": "[ผู้龍ṲṲṛḡḛḛṇṭ龍ผู้]",
"approval-requests.request.ticket-details.requester": "[ผู้龍Ṛḛḛʠṵṵḛḛṡṭḛḛṛ龍ผู้]",
"approval-requests.status.approved": "[ผู้龍ḀḀṗṗṛṓṓṽḛḛḍ龍ผู้]",
"approval-requests.status.decision-pending": "[ผู้龍Ḍḛḛͼḭḭṡḭḭṓṓṇ ṗḛḛṇḍḭḭṇḡ龍ผู้]",
"approval-requests.status.denied": "[ผู้龍Ḍḛḛṇḭḭḛḛḍ龍ผู้]",
"approval-requests.status.info-needed": "[ผู้龍ḬḬṇϝṓṓ ṇḛḛḛḛḍḛḛḍ龍ผู้]",
"approval-requests.status.withdrawn": "[ผู้龍Ŵḭḭṭḥḍṛααẁṇ龍ผู้]"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "طلبات الموافقة",
"approval-requests.list.no-requests": "لم يتم العثور على طلبات الموافقة.",
"approval-requests.list.search-placeholder": "طلبات الموافقة على البحث",
"approval-requests.list.status-dropdown.any": "أي عنصر",
"approval-requests.list.status-dropdown.label_v2": "الحالة",
"approval-requests.list.table.approval-status": "حالة الموافقة",
"approval-requests.list.table.requester": "الطالب",
"approval-requests.list.table.sent-by": "تم الإرسال من قبل",
"approval-requests.list.table.sent-on": "تاريخ الإرسال",
"approval-requests.list.table.subject": "الموضوع",
"approval-requests.request.approval-request-details.approver": "جهة الموافقة",
"approval-requests.request.approval-request-details.comment": "التعليق",
"approval-requests.request.approval-request-details.decided": "تاريخ القرار",
"approval-requests.request.approval-request-details.header": "تفاصيل طلب الموافقة",
"approval-requests.request.approval-request-details.previous-decision": "القرار السابق",
"approval-requests.request.approval-request-details.sent-by": "تم الإرسال من قبل",
"approval-requests.request.approval-request-details.sent-on": "تاريخ الإرسال",
"approval-requests.request.approval-request-details.status": "الحالة",
"approval-requests.request.approval-request-details.withdrawn-on": "تاريخ السحب",
"approval-requests.request.approver-actions.additional-note-label": "ملاحظة إضافية",
"approval-requests.request.approver-actions.approve-request": "الموافقة على الطلب",
"approval-requests.request.approver-actions.cancel": "إلغاء",
"approval-requests.request.approver-actions.denial-reason-label": "سبب الرفض* (مطلوب)",
"approval-requests.request.approver-actions.denial-reason-validation": "أدخل سببًا للرفض",
"approval-requests.request.approver-actions.deny-request": "رفض الطلب",
"approval-requests.request.approver-actions.submit-approval": "إرسال الموافقة",
"approval-requests.request.approver-actions.submit-denial": "إرسال الرفض",
"approval-requests.request.notification.approval-submitted": "تم إرسال الموافقة",
"approval-requests.request.notification.denial-submitted": "تم إرسال الرفض",
"approval-requests.request.ticket-details.checkbox-value.no": "لا",
"approval-requests.request.ticket-details.checkbox-value.yes": "نعم",
"approval-requests.request.ticket-details.header": "تفاصيل التذكرة",
"approval-requests.request.ticket-details.id": "المُعرِّف",
"approval-requests.request.ticket-details.priority": "الأولوية",
"approval-requests.request.ticket-details.priority_high": "عالية",
"approval-requests.request.ticket-details.priority_low": "منخفضة",
"approval-requests.request.ticket-details.priority_normal": "عادية",
"approval-requests.request.ticket-details.priority_urgent": "مستعجلة",
"approval-requests.request.ticket-details.requester": "الطالب",
"approval-requests.status.approved": "مقبول",
"approval-requests.status.decision-pending": "بانتظار صدور قرار",
"approval-requests.status.denied": "مرفوض",
"approval-requests.status.info-needed": "هناك حاجة إلى معلومات",
"approval-requests.status.withdrawn": "تم السحب"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Запросы на утверждение",
"approval-requests.list.no-requests": "Нет запросов на утверждение.",
"approval-requests.list.search-placeholder": "Поиск запросов на утверждение",
"approval-requests.list.status-dropdown.any": "Любой",
"approval-requests.list.status-dropdown.label_v2": "Статус",
"approval-requests.list.table.approval-status": "Статус утверждения",
"approval-requests.list.table.requester": "Инициатор",
"approval-requests.list.table.sent-by": "Отправитель",
"approval-requests.list.table.sent-on": "Дата отправки",
"approval-requests.list.table.subject": "Тема",
"approval-requests.request.approval-request-details.approver": "Утверждающий пользователь",
"approval-requests.request.approval-request-details.comment": "Комментарий",
"approval-requests.request.approval-request-details.decided": "Принято",
"approval-requests.request.approval-request-details.header": "Сведения о запросе на утверждение",
"approval-requests.request.approval-request-details.previous-decision": "Предыдущее решение",
"approval-requests.request.approval-request-details.sent-by": "Отправитель",
"approval-requests.request.approval-request-details.sent-on": "Дата отправки",
"approval-requests.request.approval-request-details.status": "Статус",
"approval-requests.request.approval-request-details.withdrawn-on": "Дата аннулирования",
"approval-requests.request.approver-actions.additional-note-label": "Дополнительное примечание",
"approval-requests.request.approver-actions.approve-request": "Утвердить запрос",
"approval-requests.request.approver-actions.cancel": "Отмена",
"approval-requests.request.approver-actions.denial-reason-label": "Причина отказа* (обязательное поле)",
"approval-requests.request.approver-actions.denial-reason-validation": "Введите причину отказа",
"approval-requests.request.approver-actions.deny-request": "Отклонить запрос",
"approval-requests.request.approver-actions.submit-approval": "Отправить утверждение",
"approval-requests.request.approver-actions.submit-denial": "Отправить отказ",
"approval-requests.request.notification.approval-submitted": "Утверждение отправлено",
"approval-requests.request.notification.denial-submitted": "Отказ отправлен",
"approval-requests.request.ticket-details.checkbox-value.no": "Нет",
"approval-requests.request.ticket-details.checkbox-value.yes": "Да",
"approval-requests.request.ticket-details.header": "Сведения о тикете",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Приоритет",
"approval-requests.request.ticket-details.priority_high": "Высокий",
"approval-requests.request.ticket-details.priority_low": "Низкий",
"approval-requests.request.ticket-details.priority_normal": "Нормальный",
"approval-requests.request.ticket-details.priority_urgent": "Экстренный",
"approval-requests.request.ticket-details.requester": "Инициатор",
"approval-requests.status.approved": "Утверждено",
"approval-requests.status.decision-pending": "В ожидании решения",
"approval-requests.status.denied": "Отклонено",
"approval-requests.status.info-needed": "Требуется информация",
"approval-requests.status.withdrawn": "Аннулировано"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Заявки за одобрение",
"approval-requests.list.no-requests": "Не са намерени заявки за одобрение.",
"approval-requests.list.search-placeholder": "Търсене на заявки за одобрение",
"approval-requests.list.status-dropdown.any": "Всеки",
"approval-requests.list.status-dropdown.label_v2": "Статус",
"approval-requests.list.table.approval-status": "Статус на одобрение",
"approval-requests.list.table.requester": "Заявител",
"approval-requests.list.table.sent-by": "Изпратена от",
"approval-requests.list.table.sent-on": "Изпратена на",
"approval-requests.list.table.subject": "Тема",
"approval-requests.request.approval-request-details.approver": "Одобряващ",
"approval-requests.request.approval-request-details.comment": "Коментар",
"approval-requests.request.approval-request-details.decided": "Решено",
"approval-requests.request.approval-request-details.header": "Подробности за заявката за одобрение",
"approval-requests.request.approval-request-details.previous-decision": "Предишно решение",
"approval-requests.request.approval-request-details.sent-by": "Изпратена от",
"approval-requests.request.approval-request-details.sent-on": "Изпратена на",
"approval-requests.request.approval-request-details.status": "Статус",
"approval-requests.request.approval-request-details.withdrawn-on": "Оттеглена на",
"approval-requests.request.approver-actions.additional-note-label": "Допълнителна забележка",
"approval-requests.request.approver-actions.approve-request": "Одобряване на заявката",
"approval-requests.request.approver-actions.cancel": "Отказ",
"approval-requests.request.approver-actions.denial-reason-label": "Причина за отказ* (задължително)",
"approval-requests.request.approver-actions.denial-reason-validation": "Въведете причина за отказ",
"approval-requests.request.approver-actions.deny-request": "Отхвърляне на заявката",
"approval-requests.request.approver-actions.submit-approval": "Подаване на одобрение",
"approval-requests.request.approver-actions.submit-denial": "Подаване на отхвърляне",
"approval-requests.request.notification.approval-submitted": "Одобрението е подадено",
"approval-requests.request.notification.denial-submitted": "Отхвърлянето е подадено",
"approval-requests.request.ticket-details.checkbox-value.no": "Не",
"approval-requests.request.ticket-details.checkbox-value.yes": "Да",
"approval-requests.request.ticket-details.header": "Подробности за казуса",
"approval-requests.request.ticket-details.id": "Идентификатор",
"approval-requests.request.ticket-details.priority": "Приоритет",
"approval-requests.request.ticket-details.priority_high": "Висок",
"approval-requests.request.ticket-details.priority_low": "Нисък",
"approval-requests.request.ticket-details.priority_normal": "Нормален",
"approval-requests.request.ticket-details.priority_urgent": "Спешен",
"approval-requests.request.ticket-details.requester": "Заявител",
"approval-requests.status.approved": "Одобрен",
"approval-requests.status.decision-pending": "Чакащ решение",
"approval-requests.status.denied": "Отказан",
"approval-requests.status.info-needed": "Нужна е информация",
"approval-requests.status.withdrawn": "Оттеглен"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Požadavky na schválení",
"approval-requests.list.no-requests": "Nebyly nalezeny žádné požadavky na schválení.",
"approval-requests.list.search-placeholder": "Hledejte požadavky na schválení",
"approval-requests.list.status-dropdown.any": "Jakékoli",
"approval-requests.list.status-dropdown.label_v2": "Stav",
"approval-requests.list.table.approval-status": "Stav schválení",
"approval-requests.list.table.requester": "Klient",
"approval-requests.list.table.sent-by": "Odeslal/a",
"approval-requests.list.table.sent-on": "Odesláno dne",
"approval-requests.list.table.subject": "Předmět",
"approval-requests.request.approval-request-details.approver": "Schvalovatel",
"approval-requests.request.approval-request-details.comment": "Komentář",
"approval-requests.request.approval-request-details.decided": "Rozhodnutí",
"approval-requests.request.approval-request-details.header": "Podrobnosti požadavku na schválení",
"approval-requests.request.approval-request-details.previous-decision": "Předchozí rozhodnutí",
"approval-requests.request.approval-request-details.sent-by": "Odeslal/a",
"approval-requests.request.approval-request-details.sent-on": "Odesláno dne",
"approval-requests.request.approval-request-details.status": "Stav",
"approval-requests.request.approval-request-details.withdrawn-on": "Staženo dne",
"approval-requests.request.approver-actions.additional-note-label": "Dodatečná poznámka",
"approval-requests.request.approver-actions.approve-request": "Schválit požadavek",
"approval-requests.request.approver-actions.cancel": "Zrušit",
"approval-requests.request.approver-actions.denial-reason-label": "Důvod zamítnutí* (povinné pole)",
"approval-requests.request.approver-actions.denial-reason-validation": "Zadejte důvod zamítnutí.",
"approval-requests.request.approver-actions.deny-request": "Zamítnout požadavek",
"approval-requests.request.approver-actions.submit-approval": "Odeslat schválení",
"approval-requests.request.approver-actions.submit-denial": "Odeslat zamítnutí",
"approval-requests.request.notification.approval-submitted": "Schválení odesláno",
"approval-requests.request.notification.denial-submitted": "Zamítnutí odesláno",
"approval-requests.request.ticket-details.checkbox-value.no": "Ne",
"approval-requests.request.ticket-details.checkbox-value.yes": "Ano",
"approval-requests.request.ticket-details.header": "Detaily tiketu",
"approval-requests.request.ticket-details.id": "Identifikátor",
"approval-requests.request.ticket-details.priority": "Priorita",
"approval-requests.request.ticket-details.priority_high": "Vysoká",
"approval-requests.request.ticket-details.priority_low": "Nízká",
"approval-requests.request.ticket-details.priority_normal": "Normální",
"approval-requests.request.ticket-details.priority_urgent": "Naléhavá",
"approval-requests.request.ticket-details.requester": "Klient",
"approval-requests.status.approved": "Schváleno",
"approval-requests.status.decision-pending": "Čeká se na rozhodnutí",
"approval-requests.status.denied": "Zamítnuto",
"approval-requests.status.info-needed": "Požadované informace",
"approval-requests.status.withdrawn": "Staženo"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Anmodninger om godkendelse",
"approval-requests.list.no-requests": "Der blev ikke fundet nogen anmodninger om godkendelse.",
"approval-requests.list.search-placeholder": "Søg i anmodninger om godkendelse",
"approval-requests.list.status-dropdown.any": "Alle",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Godkendelsesstatus",
"approval-requests.list.table.requester": "Anmoder",
"approval-requests.list.table.sent-by": "Sendt af",
"approval-requests.list.table.sent-on": "Sendt d.",
"approval-requests.list.table.subject": "Emne",
"approval-requests.request.approval-request-details.approver": "Godkender",
"approval-requests.request.approval-request-details.comment": "Kommentar",
"approval-requests.request.approval-request-details.decided": "Afgjort",
"approval-requests.request.approval-request-details.header": "Oplysninger om anmodning om godkendelse",
"approval-requests.request.approval-request-details.previous-decision": "Tidligere beslutning",
"approval-requests.request.approval-request-details.sent-by": "Sendt af",
"approval-requests.request.approval-request-details.sent-on": "Sendt d.",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Trukket tilbage d.",
"approval-requests.request.approver-actions.additional-note-label": "Yderligere bemærkning",
"approval-requests.request.approver-actions.approve-request": "Godkend anmodning",
"approval-requests.request.approver-actions.cancel": "Annuller",
"approval-requests.request.approver-actions.denial-reason-label": "Årsag til afvisning* (obligatorisk)",
"approval-requests.request.approver-actions.denial-reason-validation": "Indtast en årsag til afvisningen",
"approval-requests.request.approver-actions.deny-request": "Afvis anmodning",
"approval-requests.request.approver-actions.submit-approval": "Indsend godkendelse",
"approval-requests.request.approver-actions.submit-denial": "Indsend afvisning",
"approval-requests.request.notification.approval-submitted": "Godkendelse indsendt",
"approval-requests.request.notification.denial-submitted": "Afvisning indsendt",
"approval-requests.request.ticket-details.checkbox-value.no": "Nej",
"approval-requests.request.ticket-details.checkbox-value.yes": "Ja",
"approval-requests.request.ticket-details.header": "Oplysninger om ticket",
"approval-requests.request.ticket-details.id": "Id",
"approval-requests.request.ticket-details.priority": "Prioritet",
"approval-requests.request.ticket-details.priority_high": "Høj",
"approval-requests.request.ticket-details.priority_low": "Lav",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Haster",
"approval-requests.request.ticket-details.requester": "Anmoder",
"approval-requests.status.approved": "Godkendt",
"approval-requests.status.decision-pending": "Afgørelse venter",
"approval-requests.status.denied": "Afvist",
"approval-requests.status.info-needed": "Oplysninger nødvendige",
"approval-requests.status.withdrawn": "Tilbagetrukket"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Genehmigungsanfragen",
"approval-requests.list.no-requests": "Keine Genehmigungsanfragen gefunden.",
"approval-requests.list.search-placeholder": "Genehmigungsanfragen suchen",
"approval-requests.list.status-dropdown.any": "Beliebig",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Genehmigungsstatus",
"approval-requests.list.table.requester": "Anfragender",
"approval-requests.list.table.sent-by": "Gesendet von",
"approval-requests.list.table.sent-on": "Gesendet am",
"approval-requests.list.table.subject": "Betreff",
"approval-requests.request.approval-request-details.approver": "Genehmiger",
"approval-requests.request.approval-request-details.comment": "Kommentar",
"approval-requests.request.approval-request-details.decided": "Entschieden",
"approval-requests.request.approval-request-details.header": "Details zur Genehmigungsanfrage",
"approval-requests.request.approval-request-details.previous-decision": "Vorherige Entscheidung",
"approval-requests.request.approval-request-details.sent-by": "Gesendet von",
"approval-requests.request.approval-request-details.sent-on": "Gesendet am",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Zurückgezogen am",
"approval-requests.request.approver-actions.additional-note-label": "Zusätzlicher Hinweis",
"approval-requests.request.approver-actions.approve-request": "Anfrage genehmigen",
"approval-requests.request.approver-actions.cancel": "Abbrechen",
"approval-requests.request.approver-actions.denial-reason-label": "Grund für Ablehnung* (erforderlich)",
"approval-requests.request.approver-actions.denial-reason-validation": "Grund für die Ablehnung eingeben",
"approval-requests.request.approver-actions.deny-request": "Anfrage ablehnen",
"approval-requests.request.approver-actions.submit-approval": "Genehmigung senden",
"approval-requests.request.approver-actions.submit-denial": "Ablehnung senden",
"approval-requests.request.notification.approval-submitted": "Genehmigung gesendet",
"approval-requests.request.notification.denial-submitted": "Ablehnung gesendet",
"approval-requests.request.ticket-details.checkbox-value.no": "Nein",
"approval-requests.request.ticket-details.checkbox-value.yes": "Ja",
"approval-requests.request.ticket-details.header": "Ticketdetails",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priorität",
"approval-requests.request.ticket-details.priority_high": "Hoch",
"approval-requests.request.ticket-details.priority_low": "Niedrig",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Dringend",
"approval-requests.request.ticket-details.requester": "Anfragender",
"approval-requests.status.approved": "Genehmigt",
"approval-requests.status.decision-pending": "Entscheidung ausstehend",
"approval-requests.status.denied": "Abgelehnt",
"approval-requests.status.info-needed": "Weitere Informationen erforderlich",
"approval-requests.status.withdrawn": "Zurückgezogen"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Genehmigungsanfragen",
"approval-requests.list.no-requests": "Keine Genehmigungsanfragen gefunden.",
"approval-requests.list.search-placeholder": "Genehmigungsanfragen suchen",
"approval-requests.list.status-dropdown.any": "Beliebig",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Genehmigungsstatus",
"approval-requests.list.table.requester": "Anfragender",
"approval-requests.list.table.sent-by": "Gesendet von",
"approval-requests.list.table.sent-on": "Gesendet am",
"approval-requests.list.table.subject": "Betreff",
"approval-requests.request.approval-request-details.approver": "Genehmiger",
"approval-requests.request.approval-request-details.comment": "Kommentar",
"approval-requests.request.approval-request-details.decided": "Entschieden",
"approval-requests.request.approval-request-details.header": "Details zur Genehmigungsanfrage",
"approval-requests.request.approval-request-details.previous-decision": "Vorherige Entscheidung",
"approval-requests.request.approval-request-details.sent-by": "Gesendet von",
"approval-requests.request.approval-request-details.sent-on": "Gesendet am",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Zurückgezogen am",
"approval-requests.request.approver-actions.additional-note-label": "Zusätzlicher Hinweis",
"approval-requests.request.approver-actions.approve-request": "Anfrage genehmigen",
"approval-requests.request.approver-actions.cancel": "Abbrechen",
"approval-requests.request.approver-actions.denial-reason-label": "Grund für Ablehnung* (erforderlich)",
"approval-requests.request.approver-actions.denial-reason-validation": "Grund für die Ablehnung eingeben",
"approval-requests.request.approver-actions.deny-request": "Anfrage ablehnen",
"approval-requests.request.approver-actions.submit-approval": "Genehmigung senden",
"approval-requests.request.approver-actions.submit-denial": "Ablehnung senden",
"approval-requests.request.notification.approval-submitted": "Genehmigung gesendet",
"approval-requests.request.notification.denial-submitted": "Ablehnung gesendet",
"approval-requests.request.ticket-details.checkbox-value.no": "Nein",
"approval-requests.request.ticket-details.checkbox-value.yes": "Ja",
"approval-requests.request.ticket-details.header": "Ticketdetails",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priorität",
"approval-requests.request.ticket-details.priority_high": "Hoch",
"approval-requests.request.ticket-details.priority_low": "Niedrig",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Dringend",
"approval-requests.request.ticket-details.requester": "Anfragender",
"approval-requests.status.approved": "Genehmigt",
"approval-requests.status.decision-pending": "Entscheidung ausstehend",
"approval-requests.status.denied": "Abgelehnt",
"approval-requests.status.info-needed": "Weitere Informationen erforderlich",
"approval-requests.status.withdrawn": "Zurückgezogen"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Genehmigungsanfragen",
"approval-requests.list.no-requests": "Keine Genehmigungsanfragen gefunden.",
"approval-requests.list.search-placeholder": "Genehmigungsanfragen suchen",
"approval-requests.list.status-dropdown.any": "Beliebig",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Genehmigungsstatus",
"approval-requests.list.table.requester": "Anfragender",
"approval-requests.list.table.sent-by": "Gesendet von",
"approval-requests.list.table.sent-on": "Gesendet am",
"approval-requests.list.table.subject": "Betreff",
"approval-requests.request.approval-request-details.approver": "Genehmiger",
"approval-requests.request.approval-request-details.comment": "Kommentar",
"approval-requests.request.approval-request-details.decided": "Entschieden",
"approval-requests.request.approval-request-details.header": "Details zur Genehmigungsanfrage",
"approval-requests.request.approval-request-details.previous-decision": "Vorherige Entscheidung",
"approval-requests.request.approval-request-details.sent-by": "Gesendet von",
"approval-requests.request.approval-request-details.sent-on": "Gesendet am",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Zurückgezogen am",
"approval-requests.request.approver-actions.additional-note-label": "Zusätzlicher Hinweis",
"approval-requests.request.approver-actions.approve-request": "Anfrage genehmigen",
"approval-requests.request.approver-actions.cancel": "Abbrechen",
"approval-requests.request.approver-actions.denial-reason-label": "Grund für Ablehnung* (erforderlich)",
"approval-requests.request.approver-actions.denial-reason-validation": "Grund für die Ablehnung eingeben",
"approval-requests.request.approver-actions.deny-request": "Anfrage ablehnen",
"approval-requests.request.approver-actions.submit-approval": "Genehmigung senden",
"approval-requests.request.approver-actions.submit-denial": "Ablehnung senden",
"approval-requests.request.notification.approval-submitted": "Genehmigung gesendet",
"approval-requests.request.notification.denial-submitted": "Ablehnung gesendet",
"approval-requests.request.ticket-details.checkbox-value.no": "Nein",
"approval-requests.request.ticket-details.checkbox-value.yes": "Ja",
"approval-requests.request.ticket-details.header": "Ticketdetails",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priorität",
"approval-requests.request.ticket-details.priority_high": "Hoch",
"approval-requests.request.ticket-details.priority_low": "Niedrig",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Dringend",
"approval-requests.request.ticket-details.requester": "Anfragender",
"approval-requests.status.approved": "Genehmigt",
"approval-requests.status.decision-pending": "Entscheidung ausstehend",
"approval-requests.status.denied": "Abgelehnt",
"approval-requests.status.info-needed": "Weitere Informationen erforderlich",
"approval-requests.status.withdrawn": "Zurückgezogen"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Αιτήματα έγκρισης",
"approval-requests.list.no-requests": "Δεν βρέθηκαν αιτήματα έγκρισης.",
"approval-requests.list.search-placeholder": "Αναζητήστε αιτήματα έγκρισης",
"approval-requests.list.status-dropdown.any": "Οποιαδήποτε",
"approval-requests.list.status-dropdown.label_v2": "Κατάσταση",
"approval-requests.list.table.approval-status": "Κατάσταση έγκρισης",
"approval-requests.list.table.requester": "Αιτών",
"approval-requests.list.table.sent-by": "Στάλθηκε από",
"approval-requests.list.table.sent-on": "Στάλθηκε στις",
"approval-requests.list.table.subject": "Θέμα",
"approval-requests.request.approval-request-details.approver": "Υπεύθυνος έγκρισης",
"approval-requests.request.approval-request-details.comment": "Σχόλιο",
"approval-requests.request.approval-request-details.decided": "Αποφασίστηκε",
"approval-requests.request.approval-request-details.header": "Λεπτομέρειες αιτήματος έγκρισης",
"approval-requests.request.approval-request-details.previous-decision": "Προηγούμενη απόφαση",
"approval-requests.request.approval-request-details.sent-by": "Στάλθηκε από",
"approval-requests.request.approval-request-details.sent-on": "Στάλθηκε στις",
"approval-requests.request.approval-request-details.status": "Κατάσταση",
"approval-requests.request.approval-request-details.withdrawn-on": "Ανακλήθηκε στις",
"approval-requests.request.approver-actions.additional-note-label": "Πρόσθετη σημείωση",
"approval-requests.request.approver-actions.approve-request": "Έγκριση αιτήματος",
"approval-requests.request.approver-actions.cancel": "Ακύρωση",
"approval-requests.request.approver-actions.denial-reason-label": "Λόγος απόρριψης* (απαιτείται)",
"approval-requests.request.approver-actions.denial-reason-validation": "Εισαγάγετε έναν λόγο απόρριψης",
"approval-requests.request.approver-actions.deny-request": "Απόρριψη αιτήματος",
"approval-requests.request.approver-actions.submit-approval": "Υποβολή έγκρισης",
"approval-requests.request.approver-actions.submit-denial": "Υποβολή απόρριψης",
"approval-requests.request.notification.approval-submitted": "Η έγκριση υποβλήθηκε",
"approval-requests.request.notification.denial-submitted": "Η απόρριψη υποβλήθηκε",
"approval-requests.request.ticket-details.checkbox-value.no": "Όχι",
"approval-requests.request.ticket-details.checkbox-value.yes": "Ναι",
"approval-requests.request.ticket-details.header": "Λεπτομέρειες δελτίου",
"approval-requests.request.ticket-details.id": "Αναγνωριστικό",
"approval-requests.request.ticket-details.priority": "Προτεραιότητα",
"approval-requests.request.ticket-details.priority_high": "Υψηλή",
"approval-requests.request.ticket-details.priority_low": "Χαμηλή",
"approval-requests.request.ticket-details.priority_normal": "Κανονική",
"approval-requests.request.ticket-details.priority_urgent": "Επείγουσα",
"approval-requests.request.ticket-details.requester": "Αιτών",
"approval-requests.status.approved": "Εγκρίθηκε",
"approval-requests.status.decision-pending": "Απόφαση σε εκκρεμότητα",
"approval-requests.status.denied": "Απορρίφθηκε",
"approval-requests.status.info-needed": "Χρειάζονται πληροφορίες",
"approval-requests.status.withdrawn": "Ανακλήθηκε"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "approval-requests.list.header",
"approval-requests.list.no-requests": "approval-requests.list.no-requests",
"approval-requests.list.search-placeholder": "approval-requests.list.search-placeholder",
"approval-requests.list.status-dropdown.any": "approval-requests.list.status-dropdown.any",
"approval-requests.list.status-dropdown.label_v2": "approval-requests.list.status-dropdown.label_v2",
"approval-requests.list.table.approval-status": "approval-requests.list.table.approval-status",
"approval-requests.list.table.requester": "approval-requests.list.table.requester",
"approval-requests.list.table.sent-by": "approval-requests.list.table.sent-by",
"approval-requests.list.table.sent-on": "approval-requests.list.table.sent-on",
"approval-requests.list.table.subject": "approval-requests.list.table.subject",
"approval-requests.request.approval-request-details.approver": "approval-requests.request.approval-request-details.approver",
"approval-requests.request.approval-request-details.comment": "approval-requests.request.approval-request-details.comment",
"approval-requests.request.approval-request-details.decided": "approval-requests.request.approval-request-details.decided",
"approval-requests.request.approval-request-details.header": "approval-requests.request.approval-request-details.header",
"approval-requests.request.approval-request-details.previous-decision": "approval-requests.request.approval-request-details.previous-decision",
"approval-requests.request.approval-request-details.sent-by": "approval-requests.request.approval-request-details.sent-by",
"approval-requests.request.approval-request-details.sent-on": "approval-requests.request.approval-request-details.sent-on",
"approval-requests.request.approval-request-details.status": "approval-requests.request.approval-request-details.status",
"approval-requests.request.approval-request-details.withdrawn-on": "approval-requests.request.approval-request-details.withdrawn-on",
"approval-requests.request.approver-actions.additional-note-label": "approval-requests.request.approver-actions.additional-note-label",
"approval-requests.request.approver-actions.approve-request": "approval-requests.request.approver-actions.approve-request",
"approval-requests.request.approver-actions.cancel": "approval-requests.request.approver-actions.cancel",
"approval-requests.request.approver-actions.denial-reason-label": "approval-requests.request.approver-actions.denial-reason-label",
"approval-requests.request.approver-actions.denial-reason-validation": "approval-requests.request.approver-actions.denial-reason-validation",
"approval-requests.request.approver-actions.deny-request": "approval-requests.request.approver-actions.deny-request",
"approval-requests.request.approver-actions.submit-approval": "approval-requests.request.approver-actions.submit-approval",
"approval-requests.request.approver-actions.submit-denial": "approval-requests.request.approver-actions.submit-denial",
"approval-requests.request.notification.approval-submitted": "approval-requests.request.notification.approval-submitted",
"approval-requests.request.notification.denial-submitted": "approval-requests.request.notification.denial-submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "approval-requests.request.ticket-details.checkbox-value.no",
"approval-requests.request.ticket-details.checkbox-value.yes": "approval-requests.request.ticket-details.checkbox-value.yes",
"approval-requests.request.ticket-details.header": "approval-requests.request.ticket-details.header",
"approval-requests.request.ticket-details.id": "approval-requests.request.ticket-details.id",
"approval-requests.request.ticket-details.priority": "approval-requests.request.ticket-details.priority",
"approval-requests.request.ticket-details.priority_high": "approval-requests.request.ticket-details.priority_high",
"approval-requests.request.ticket-details.priority_low": "approval-requests.request.ticket-details.priority_low",
"approval-requests.request.ticket-details.priority_normal": "approval-requests.request.ticket-details.priority_normal",
"approval-requests.request.ticket-details.priority_urgent": "approval-requests.request.ticket-details.priority_urgent",
"approval-requests.request.ticket-details.requester": "approval-requests.request.ticket-details.requester",
"approval-requests.status.approved": "approval-requests.status.approved",
"approval-requests.status.decision-pending": "approval-requests.status.decision-pending",
"approval-requests.status.denied": "approval-requests.status.denied",
"approval-requests.status.info-needed": "approval-requests.status.info-needed",
"approval-requests.status.withdrawn": "approval-requests.status.withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭṡ龍ผู้]",
"approval-requests.list.no-requests": "[ผู้龍Ṅṓṓ ααṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭṡ ϝṓṓṵṵṇḍ.龍ผู้]",
"approval-requests.list.search-placeholder": "[ผู้龍Ṣḛḛααṛͼḥ ααṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭṡ龍ผู้]",
"approval-requests.list.status-dropdown.any": "[ผู้龍ḀḀṇẏẏ龍ผู้]",
"approval-requests.list.status-dropdown.label_v2": "[ผู้龍Ṣṭααṭṵṵṡ龍ผู้]",
"approval-requests.list.table.approval-status": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṡṭααṭṵṵṡ龍ผู้]",
"approval-requests.list.table.requester": "[ผู้龍Ṛḛḛʠṵṵḛḛṡṭḛḛṛ龍ผู้]",
"approval-requests.list.table.sent-by": "[ผู้龍Ṣḛḛṇṭ ḅẏẏ龍ผู้]",
"approval-requests.list.table.sent-on": "[ผู้龍Ṣḛḛṇṭ ṓṓṇ龍ผู้]",
"approval-requests.list.table.subject": "[ผู้龍Ṣṵṵḅĵḛḛͼṭ龍ผู้]",
"approval-requests.request.approval-request-details.approver": "[ผู้龍ḀḀṗṗṛṓṓṽḛḛṛ龍ผู้]",
"approval-requests.request.approval-request-details.comment": "[ผู้龍Ḉṓṓṃṃḛḛṇṭ龍ผู้]",
"approval-requests.request.approval-request-details.decided": "[ผู้龍Ḍḛḛͼḭḭḍḛḛḍ龍ผู้]",
"approval-requests.request.approval-request-details.header": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṛḛḛʠṵṵḛḛṡṭ ḍḛḛṭααḭḭḽṡ龍ผู้]",
"approval-requests.request.approval-request-details.previous-decision": "[ผู้龍Ṕṛḛḛṽḭḭṓṓṵṵṡ ḍḛḛͼḭḭṡḭḭṓṓṇ龍ผู้]",
"approval-requests.request.approval-request-details.sent-by": "[ผู้龍Ṣḛḛṇṭ ḅẏẏ龍ผู้]",
"approval-requests.request.approval-request-details.sent-on": "[ผู้龍Ṣḛḛṇṭ ṓṓṇ龍ผู้]",
"approval-requests.request.approval-request-details.status": "[ผู้龍Ṣṭααṭṵṵṡ龍ผู้]",
"approval-requests.request.approval-request-details.withdrawn-on": "[ผู้龍Ŵḭḭṭḥḍṛααẁṇ ṓṓṇ龍ผู้]",
"approval-requests.request.approver-actions.additional-note-label": "[ผู้龍ḀḀḍḍḭḭṭḭḭṓṓṇααḽ ṇṓṓṭḛḛ龍ผู้]",
"approval-requests.request.approver-actions.approve-request": "[ผู้龍ḀḀṗṗṛṓṓṽḛḛ ṛḛḛʠṵṵḛḛṡṭ龍ผู้]",
"approval-requests.request.approver-actions.cancel": "[ผู้龍Ḉααṇͼḛḛḽ龍ผู้]",
"approval-requests.request.approver-actions.denial-reason-label": "[ผู้龍Ṛḛḛααṡṓṓṇ ϝṓṓṛ ḍḛḛṇḭḭααḽ* (Ṛḛḛʠṵṵḭḭṛḛḛḍ)龍ผู้]",
"approval-requests.request.approver-actions.denial-reason-validation": "[ผู้龍ḚḚṇṭḛḛṛ αα ṛḛḛααṡṓṓṇ ϝṓṓṛ ḍḛḛṇḭḭααḽ龍ผู้]",
"approval-requests.request.approver-actions.deny-request": "[ผู้龍Ḍḛḛṇẏẏ ṛḛḛʠṵṵḛḛṡṭ龍ผู้]",
"approval-requests.request.approver-actions.submit-approval": "[ผู้龍Ṣṵṵḅṃḭḭṭ ααṗṗṛṓṓṽααḽ龍ผู้]",
"approval-requests.request.approver-actions.submit-denial": "[ผู้龍Ṣṵṵḅṃḭḭṭ ḍḛḛṇḭḭααḽ龍ผู้]",
"approval-requests.request.notification.approval-submitted": "[ผู้龍ḀḀṗṗṛṓṓṽααḽ ṡṵṵḅṃḭḭṭṭḛḛḍ龍ผู้]",
"approval-requests.request.notification.denial-submitted": "[ผู้龍Ḍḛḛṇḭḭααḽ ṡṵṵḅṃḭḭṭṭḛḛḍ龍ผู้]",
"approval-requests.request.ticket-details.checkbox-value.no": "[ผู้龍Ṅṓṓ龍ผู้]",
"approval-requests.request.ticket-details.checkbox-value.yes": "[ผู้龍ŶŶḛḛṡ龍ผู้]",
"approval-requests.request.ticket-details.header": "[ผู้龍Ṫḭḭͼḳḛḛṭ ḍḛḛṭααḭḭḽṡ龍ผู้]",
"approval-requests.request.ticket-details.id": "[ผู้龍ḬḬḌ龍ผู้]",
"approval-requests.request.ticket-details.priority": "[ผู้龍Ṕṛḭḭṓṓṛḭḭṭẏẏ龍ผู้]",
"approval-requests.request.ticket-details.priority_high": "[ผู้龍Ḥḭḭḡḥ龍ผู้]",
"approval-requests.request.ticket-details.priority_low": "[ผู้龍Ḻṓṓẁ龍ผู้]",
"approval-requests.request.ticket-details.priority_normal": "[ผู้龍Ṅṓṓṛṃααḽ龍ผู้]",
"approval-requests.request.ticket-details.priority_urgent": "[ผู้龍ṲṲṛḡḛḛṇṭ龍ผู้]",
"approval-requests.request.ticket-details.requester": "[ผู้龍Ṛḛḛʠṵṵḛḛṡṭḛḛṛ龍ผู้]",
"approval-requests.status.approved": "[ผู้龍ḀḀṗṗṛṓṓṽḛḛḍ龍ผู้]",
"approval-requests.status.decision-pending": "[ผู้龍Ḍḛḛͼḭḭṡḭḭṓṓṇ ṗḛḛṇḍḭḭṇḡ龍ผู้]",
"approval-requests.status.denied": "[ผู้龍Ḍḛḛṇḭḭḛḛḍ龍ผู้]",
"approval-requests.status.info-needed": "[ผู้龍ḬḬṇϝṓṓ ṇḛḛḛḛḍḛḛḍ龍ผู้]",
"approval-requests.status.withdrawn": "[ผู้龍Ŵḭḭṭḥḍṛααẁṇ龍ผู้]"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Solicitudes de aprobación",
"approval-requests.list.no-requests": "No se encontraron solicitudes de aprobación.",
"approval-requests.list.search-placeholder": "Buscar solicitudes de aprobación",
"approval-requests.list.status-dropdown.any": "Cualquiera",
"approval-requests.list.status-dropdown.label_v2": "Estado",
"approval-requests.list.table.approval-status": "Estado de aprobación",
"approval-requests.list.table.requester": "Solicitante",
"approval-requests.list.table.sent-by": "Enviado por",
"approval-requests.list.table.sent-on": "Enviado el",
"approval-requests.list.table.subject": "Asunto",
"approval-requests.request.approval-request-details.approver": "Aprobador",
"approval-requests.request.approval-request-details.comment": "Comentar",
"approval-requests.request.approval-request-details.decided": "Decidido",
"approval-requests.request.approval-request-details.header": "Detalles de la solicitud de aprobación",
"approval-requests.request.approval-request-details.previous-decision": "Decisión anterior",
"approval-requests.request.approval-request-details.sent-by": "Enviado por",
"approval-requests.request.approval-request-details.sent-on": "Enviado el",
"approval-requests.request.approval-request-details.status": "Estado",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirado el",
"approval-requests.request.approver-actions.additional-note-label": "Nota adicional",
"approval-requests.request.approver-actions.approve-request": "Aprobar solicitud",
"approval-requests.request.approver-actions.cancel": "Cancelar",
"approval-requests.request.approver-actions.denial-reason-label": "Motivo de la denegación* (obligatorio)",
"approval-requests.request.approver-actions.denial-reason-validation": "Ingrese un motivo para la denegación",
"approval-requests.request.approver-actions.deny-request": "Denegar solicitud",
"approval-requests.request.approver-actions.submit-approval": "Enviar aprobación",
"approval-requests.request.approver-actions.submit-denial": "Enviar denegación",
"approval-requests.request.notification.approval-submitted": "Aprobación enviada",
"approval-requests.request.notification.denial-submitted": "Denegación enviada",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Sí",
"approval-requests.request.ticket-details.header": "Detalles del ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Prioridad",
"approval-requests.request.ticket-details.priority_high": "Alta",
"approval-requests.request.ticket-details.priority_low": "Baja",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Solicitante",
"approval-requests.status.approved": "Aprobado",
"approval-requests.status.decision-pending": "Decisión pendiente",
"approval-requests.status.denied": "Denegado",
"approval-requests.status.info-needed": "Se necesita información",
"approval-requests.status.withdrawn": "Retirado"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Hyväksyntäpyynnöt",
"approval-requests.list.no-requests": "Hyväksyntäpyyntöjä ei löytynyt.",
"approval-requests.list.search-placeholder": "Hae hyväksyntäpyyntöjä",
"approval-requests.list.status-dropdown.any": "Mikä tahansa",
"approval-requests.list.status-dropdown.label_v2": "Tila",
"approval-requests.list.table.approval-status": "Hyväksynnän tila",
"approval-requests.list.table.requester": "Pyynnön tekijä",
"approval-requests.list.table.sent-by": "Lähettäjä",
"approval-requests.list.table.sent-on": "Lähetetty",
"approval-requests.list.table.subject": "Aihe",
"approval-requests.request.approval-request-details.approver": "Hyväksyjä",
"approval-requests.request.approval-request-details.comment": "Kommentti",
"approval-requests.request.approval-request-details.decided": "Päätetty",
"approval-requests.request.approval-request-details.header": "Hyväksyntäpyynnön tiedot",
"approval-requests.request.approval-request-details.previous-decision": "Edellinen päätös",
"approval-requests.request.approval-request-details.sent-by": "Lähettäjä",
"approval-requests.request.approval-request-details.sent-on": "Lähetetty",
"approval-requests.request.approval-request-details.status": "Tila",
"approval-requests.request.approval-request-details.withdrawn-on": "Peruutettu",
"approval-requests.request.approver-actions.additional-note-label": "Lisähuomautus",
"approval-requests.request.approver-actions.approve-request": "Hyväksy pyyntö",
"approval-requests.request.approver-actions.cancel": "Peruuta",
"approval-requests.request.approver-actions.denial-reason-label": "Syy hylkäämiseen* (pakollinen)",
"approval-requests.request.approver-actions.denial-reason-validation": "Anna hylkäämisen syy",
"approval-requests.request.approver-actions.deny-request": "Hylkää pyyntö",
"approval-requests.request.approver-actions.submit-approval": "Lähetä hyväksyntä",
"approval-requests.request.approver-actions.submit-denial": "Lähetä hylkäys",
"approval-requests.request.notification.approval-submitted": "Hyväksyntä lähetetty",
"approval-requests.request.notification.denial-submitted": "Hylkäys lähetetty",
"approval-requests.request.ticket-details.checkbox-value.no": "Ei",
"approval-requests.request.ticket-details.checkbox-value.yes": "Kyllä",
"approval-requests.request.ticket-details.header": "Tiketin tiedot",
"approval-requests.request.ticket-details.id": "Tunnus",
"approval-requests.request.ticket-details.priority": "Prioriteetti",
"approval-requests.request.ticket-details.priority_high": "Korkea",
"approval-requests.request.ticket-details.priority_low": "Alhainen",
"approval-requests.request.ticket-details.priority_normal": "Normaali",
"approval-requests.request.ticket-details.priority_urgent": "Kiireellinen",
"approval-requests.request.ticket-details.requester": "Pyynnön tekijä",
"approval-requests.status.approved": "Hyväksytty",
"approval-requests.status.decision-pending": "Odottaa päätöstä",
"approval-requests.status.denied": "Hylätty",
"approval-requests.status.info-needed": "Tietoja tarvitaan",
"approval-requests.status.withdrawn": "Peruutettu"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Approval requests",
"approval-requests.list.no-requests": "No approval requests found.",
"approval-requests.list.search-placeholder": "Search approval requests",
"approval-requests.list.status-dropdown.any": "Any",
"approval-requests.list.status-dropdown.label_v2": "Status",
"approval-requests.list.table.approval-status": "Approval status",
"approval-requests.list.table.requester": "Requester",
"approval-requests.list.table.sent-by": "Sent by",
"approval-requests.list.table.sent-on": "Sent on",
"approval-requests.list.table.subject": "Subject",
"approval-requests.request.approval-request-details.approver": "Approver",
"approval-requests.request.approval-request-details.comment": "Comment",
"approval-requests.request.approval-request-details.decided": "Decided",
"approval-requests.request.approval-request-details.header": "Approval request details",
"approval-requests.request.approval-request-details.previous-decision": "Previous decision",
"approval-requests.request.approval-request-details.sent-by": "Sent by",
"approval-requests.request.approval-request-details.sent-on": "Sent on",
"approval-requests.request.approval-request-details.status": "Status",
"approval-requests.request.approval-request-details.withdrawn-on": "Withdrawn on",
"approval-requests.request.approver-actions.additional-note-label": "Additional note",
"approval-requests.request.approver-actions.approve-request": "Approve request",
"approval-requests.request.approver-actions.cancel": "Cancel",
"approval-requests.request.approver-actions.denial-reason-label": "Reason for denial* (Required)",
"approval-requests.request.approver-actions.denial-reason-validation": "Enter a reason for denial",
"approval-requests.request.approver-actions.deny-request": "Deny request",
"approval-requests.request.approver-actions.submit-approval": "Submit approval",
"approval-requests.request.approver-actions.submit-denial": "Submit denial",
"approval-requests.request.notification.approval-submitted": "Approval submitted",
"approval-requests.request.notification.denial-submitted": "Denial submitted",
"approval-requests.request.ticket-details.checkbox-value.no": "No",
"approval-requests.request.ticket-details.checkbox-value.yes": "Yes",
"approval-requests.request.ticket-details.header": "Ticket details",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priority",
"approval-requests.request.ticket-details.priority_high": "High",
"approval-requests.request.ticket-details.priority_low": "Low",
"approval-requests.request.ticket-details.priority_normal": "Normal",
"approval-requests.request.ticket-details.priority_urgent": "Urgent",
"approval-requests.request.ticket-details.requester": "Requester",
"approval-requests.status.approved": "Approved",
"approval-requests.status.decision-pending": "Decision pending",
"approval-requests.status.denied": "Denied",
"approval-requests.status.info-needed": "Info needed",
"approval-requests.status.withdrawn": "Withdrawn"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Demandes dapprobation",
"approval-requests.list.no-requests": "Pas de demande dapprobation trouvée.",
"approval-requests.list.search-placeholder": "Rechercher des demandes dapprobation",
"approval-requests.list.status-dropdown.any": "Indifférent",
"approval-requests.list.status-dropdown.label_v2": "Statut",
"approval-requests.list.table.approval-status": "Statut d'approbation",
"approval-requests.list.table.requester": "Demandeur",
"approval-requests.list.table.sent-by": "Envoyée par",
"approval-requests.list.table.sent-on": "Envoyée le",
"approval-requests.list.table.subject": "Sujet",
"approval-requests.request.approval-request-details.approver": "Approbateur",
"approval-requests.request.approval-request-details.comment": "Commentaire",
"approval-requests.request.approval-request-details.decided": "Décision",
"approval-requests.request.approval-request-details.header": "Détails de la demande dapprobation",
"approval-requests.request.approval-request-details.previous-decision": "Décision précédente",
"approval-requests.request.approval-request-details.sent-by": "Envoyée par",
"approval-requests.request.approval-request-details.sent-on": "Envoyée le",
"approval-requests.request.approval-request-details.status": "Statut",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirée le",
"approval-requests.request.approver-actions.additional-note-label": "Note supplémentaire",
"approval-requests.request.approver-actions.approve-request": "Approuver la demande",
"approval-requests.request.approver-actions.cancel": "Annuler",
"approval-requests.request.approver-actions.denial-reason-label": "Motif du refus* (obligatoire)",
"approval-requests.request.approver-actions.denial-reason-validation": "Saisissez le motif du refus",
"approval-requests.request.approver-actions.deny-request": "Refuser la demande",
"approval-requests.request.approver-actions.submit-approval": "Envoyer lapprobation",
"approval-requests.request.approver-actions.submit-denial": "Envoyer le refus",
"approval-requests.request.notification.approval-submitted": "Approbation envoyée",
"approval-requests.request.notification.denial-submitted": "Refus envoyé",
"approval-requests.request.ticket-details.checkbox-value.no": "Non",
"approval-requests.request.ticket-details.checkbox-value.yes": "Oui",
"approval-requests.request.ticket-details.header": "Détails du ticket",
"approval-requests.request.ticket-details.id": "Identifiant",
"approval-requests.request.ticket-details.priority": "Priorité",
"approval-requests.request.ticket-details.priority_high": "Élevée",
"approval-requests.request.ticket-details.priority_low": "Faible",
"approval-requests.request.ticket-details.priority_normal": "Normale",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Demandeur",
"approval-requests.status.approved": "Approuvé",
"approval-requests.status.decision-pending": "Décision en attente",
"approval-requests.status.denied": "Refusé",
"approval-requests.status.info-needed": "Informations nécessaires",
"approval-requests.status.withdrawn": "Retiré"
}

View File

@@ -0,0 +1,46 @@
{
"approval-requests.list.header": "Demandes dapprobation",
"approval-requests.list.no-requests": "Pas de demande dapprobation trouvée.",
"approval-requests.list.search-placeholder": "Rechercher des demandes dapprobation",
"approval-requests.list.status-dropdown.any": "Indifférent",
"approval-requests.list.status-dropdown.label_v2": "Statut",
"approval-requests.list.table.approval-status": "Statut dapprobation",
"approval-requests.list.table.requester": "Demandeur",
"approval-requests.list.table.sent-by": "Envoyée par",
"approval-requests.list.table.sent-on": "Envoyée le",
"approval-requests.list.table.subject": "Sujet",
"approval-requests.request.approval-request-details.approver": "Approbateur",
"approval-requests.request.approval-request-details.comment": "Commentaire",
"approval-requests.request.approval-request-details.decided": "Décision",
"approval-requests.request.approval-request-details.header": "Détails de la demande dapprobation",
"approval-requests.request.approval-request-details.previous-decision": "Décision précédente",
"approval-requests.request.approval-request-details.sent-by": "Envoyée par",
"approval-requests.request.approval-request-details.sent-on": "Envoyée le",
"approval-requests.request.approval-request-details.status": "Statut",
"approval-requests.request.approval-request-details.withdrawn-on": "Retirée le",
"approval-requests.request.approver-actions.additional-note-label": "Note supplémentaire",
"approval-requests.request.approver-actions.approve-request": "Approuver la demande",
"approval-requests.request.approver-actions.cancel": "Annuler",
"approval-requests.request.approver-actions.denial-reason-label": "Motif du refus* (obligatoire)",
"approval-requests.request.approver-actions.denial-reason-validation": "Saisissez le motif du refus",
"approval-requests.request.approver-actions.deny-request": "Refuser la demande",
"approval-requests.request.approver-actions.submit-approval": "Envoyer lapprobation",
"approval-requests.request.approver-actions.submit-denial": "Envoyer le refus",
"approval-requests.request.notification.approval-submitted": "Approbation envoyée",
"approval-requests.request.notification.denial-submitted": "Refus envoyé",
"approval-requests.request.ticket-details.checkbox-value.no": "Non",
"approval-requests.request.ticket-details.checkbox-value.yes": "Oui",
"approval-requests.request.ticket-details.header": "Détails du ticket",
"approval-requests.request.ticket-details.id": "ID",
"approval-requests.request.ticket-details.priority": "Priorité",
"approval-requests.request.ticket-details.priority_high": "Élevée",
"approval-requests.request.ticket-details.priority_low": "Basse",
"approval-requests.request.ticket-details.priority_normal": "Normale",
"approval-requests.request.ticket-details.priority_urgent": "Urgente",
"approval-requests.request.ticket-details.requester": "Demandeur",
"approval-requests.status.approved": "Approuvé",
"approval-requests.status.decision-pending": "Décision en attente",
"approval-requests.status.denied": "Refusé",
"approval-requests.status.info-needed": "Informations nécessaires",
"approval-requests.status.withdrawn": "Retiré"
}

Some files were not shown because too many files have changed in this diff Show More