@xenide-io/the-old-ui-theme 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/dist/{Chip-Cq3AiAji.d.mts → Chip-BzuNgJIa.d.mts} +81 -23
- package/dist/{Chip-Cq3AiAji.d.ts → Chip-BzuNgJIa.d.ts} +81 -23
- package/dist/{ai-message-blocknote-WABTHFY7.mjs → ai-message-blocknote-D4Z6DUCJ.mjs} +9 -3
- package/dist/chunk-2EO5VCXP.mjs +3912 -0
- package/dist/chunk-APKRZQRO.mjs +251 -0
- package/dist/chunk-BS7BZI5W.mjs +19 -0
- package/dist/chunk-IULORI3A.mjs +8 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +997 -576
- package/dist/index.mjs +49 -45
- package/dist/suite.d.mts +157 -55
- package/dist/suite.d.ts +157 -55
- package/dist/suite.js +4249 -801
- package/dist/suite.mjs +2578 -594
- package/dist/theme-boot-P-oqKVZF.d.mts +74 -0
- package/dist/theme-boot-P-oqKVZF.d.ts +74 -0
- package/dist/theme-boot.d.mts +2 -0
- package/dist/theme-boot.d.ts +2 -0
- package/dist/theme-boot.js +44 -0
- package/dist/theme-boot.mjs +8 -0
- package/dist/ui.d.mts +5 -29
- package/dist/ui.d.ts +5 -29
- package/dist/ui.js +647 -352
- package/dist/ui.mjs +10 -12
- package/docs/README.md +34 -0
- package/docs/ai-quick-reference.md +56 -0
- package/docs/component-library-improvement-plan.md +751 -0
- package/docs/component-reference.md +81 -0
- package/docs/components.md +48 -0
- package/docs/demos.md +19 -0
- package/docs/installation.md +117 -0
- package/docs/suite-audit.md +34 -0
- package/docs/theme-token-reference.md +67 -0
- package/docs/themes.md +164 -0
- package/package.json +22 -21
- package/public/fonts/OpenRunde-Bold.woff2 +0 -0
- package/public/fonts/OpenRunde-Medium.woff2 +0 -0
- package/public/fonts/OpenRunde-Regular.woff2 +0 -0
- package/public/fonts/OpenRunde-Semibold.woff2 +0 -0
- package/scripts/package-smoke.mjs +43 -0
- package/src/app/globals.css +2 -1
- package/src/components/ui/Accordion.tsx +20 -6
- package/src/components/ui/Avatar.tsx +5 -3
- package/src/components/ui/Calendar.tsx +187 -63
- package/src/components/ui/Card.test.tsx +38 -0
- package/src/components/ui/Card.tsx +77 -14
- package/src/components/ui/CollapsibleSection.tsx +12 -4
- package/src/components/ui/CommandPalette.test.tsx +91 -4
- package/src/components/ui/CommandPalette.tsx +138 -128
- package/src/components/ui/EmptyState.tsx +2 -2
- package/src/components/ui/FormField.tsx +1 -1
- package/src/components/ui/Input.tsx +42 -18
- package/src/components/ui/Kbd.tsx +18 -3
- package/src/components/ui/Modal.tsx +3 -1
- package/src/components/ui/Progress.tsx +14 -5
- package/src/components/ui/SegmentedControl.tsx +2 -2
- package/src/components/ui/SettingsLayout.tsx +1 -1
- package/src/components/ui/Stat.tsx +3 -2
- package/src/components/ui/Table.tsx +8 -3
- package/src/components/ui/Tooltip.test.tsx +70 -1
- package/src/components/ui/Tooltip.tsx +53 -29
- package/src/components/ui/Typography.tsx +103 -49
- package/src/components/ui/avatar-stat.test.tsx +23 -0
- package/src/components/ui/calendar.test.tsx +88 -0
- package/src/components/ui/disclosure.test.tsx +39 -0
- package/src/components/ui/index.ts +44 -6
- package/src/components/ui/progress.test.tsx +15 -0
- package/src/components/ui/typography.test.tsx +26 -1
- package/src/styles/suite-skin.css +8 -2
- package/src/styles/themes.css +184 -68
- package/src/suite/components/ai-message-blocknote.tsx +13 -2
- package/src/suite/components/ai-panel.tsx +41 -10
- package/src/suite/components/app-switcher.test.tsx +21 -6
- package/src/suite/components/app-switcher.tsx +7 -11
- package/src/suite/components/suite-app-layout.tsx +12 -3
- package/src/suite/components/suite-bottom-nav.tsx +0 -1
- package/src/suite/components/suite-clients-projects-directory.test.tsx +71 -0
- package/src/suite/components/suite-clients-projects-directory.tsx +943 -0
- package/src/suite/components/suite-integration-picker.test.tsx +224 -0
- package/src/suite/components/suite-integration-picker.tsx +939 -0
- package/src/suite/components/suite-integration-rules.test.tsx +157 -0
- package/src/suite/components/suite-integration-rules.tsx +471 -0
- package/src/suite/components/suite-mobile-drawer.test.tsx +18 -2
- package/src/suite/components/suite-mobile-drawer.tsx +65 -98
- package/src/suite/components/suite-notification-bell.tsx +29 -4
- package/src/suite/components/suite-sidebar.tsx +25 -25
- package/src/suite/components/suite-skeleton.tsx +2 -2
- package/src/suite/components/suite-user-menu.tsx +9 -8
- package/src/suite/components/theme-provider.test.tsx +87 -3
- package/src/suite/components/theme-provider.tsx +144 -23
- package/src/suite/components/today-ui.tsx +79 -68
- package/src/suite/components/workspace-switcher.test.tsx +56 -0
- package/src/suite/components/workspace-switcher.tsx +3 -3
- package/src/suite/icons/app-accents.ts +12 -12
- package/src/suite/icons/glyphs.ts +6 -12
- package/src/suite/index.ts +27 -0
- package/src/suite/lib/apps.test.ts +9 -1
- package/src/suite/lib/apps.ts +14 -8
- package/src/suite/lib/cookies.ts +53 -0
- package/src/suite/lib/theme-boot.ts +32 -0
- package/src/suite/lib/theme-cookie.ts +7 -0
- package/src/suite/lib/use-sidebar-width.ts +5 -51
- package/dist/chunk-KDR6HI6F.mjs +0 -1075
- package/dist/chunk-NOI42JNZ.mjs +0 -151
- package/dist/chunk-VXISSXTB.mjs +0 -2564
- package/src/suite/lib/use-lock-mobile-page-zoom.test.tsx +0 -65
- package/src/suite/lib/use-lock-mobile-page-zoom.ts +0 -51
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cleanup,
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
waitFor,
|
|
6
|
+
within,
|
|
7
|
+
} from "@testing-library/react";
|
|
8
|
+
import userEvent from "@testing-library/user-event";
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { SuiteIntegrationRules } from "@/suite/components/suite-integration-rules";
|
|
11
|
+
|
|
12
|
+
describe("SuiteIntegrationRules", () => {
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
const rule = {
|
|
16
|
+
id: "rule-1",
|
|
17
|
+
name: "GitHub issues to tasks",
|
|
18
|
+
trigger_event_type: "github.issue_assigned",
|
|
19
|
+
action_type: "create_tides_task",
|
|
20
|
+
action_config: {},
|
|
21
|
+
enabled: true,
|
|
22
|
+
last_fired_at: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it("lists rules and recent events read-only for members", () => {
|
|
26
|
+
render(
|
|
27
|
+
<SuiteIntegrationRules
|
|
28
|
+
canManage={false}
|
|
29
|
+
projects={[]}
|
|
30
|
+
rules={[rule]}
|
|
31
|
+
events={[
|
|
32
|
+
{
|
|
33
|
+
id: "evt-1",
|
|
34
|
+
provider: "github",
|
|
35
|
+
event_type: "github.issue_assigned",
|
|
36
|
+
processed_at: "2026-09-04T01:00:00Z",
|
|
37
|
+
created_at: "2026-09-04T01:00:00Z",
|
|
38
|
+
},
|
|
39
|
+
]}
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText("GitHub issues to tasks")).toBeInTheDocument();
|
|
44
|
+
expect(
|
|
45
|
+
screen.getByText("github.issue_assigned → Create a Tides task"),
|
|
46
|
+
).toBeInTheDocument();
|
|
47
|
+
expect(
|
|
48
|
+
screen.getByText("github · github.issue_assigned"),
|
|
49
|
+
).toBeInTheDocument();
|
|
50
|
+
expect(
|
|
51
|
+
screen.queryByRole("button", { name: "New rule" }),
|
|
52
|
+
).not.toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("creates a rule with the selected action configuration", async () => {
|
|
56
|
+
const user = userEvent.setup();
|
|
57
|
+
const onCreate = vi.fn().mockResolvedValue(undefined);
|
|
58
|
+
const onRefresh = vi.fn();
|
|
59
|
+
|
|
60
|
+
render(
|
|
61
|
+
<SuiteIntegrationRules
|
|
62
|
+
canManage
|
|
63
|
+
projects={[{ id: "proj-1", name: "Atlas" }]}
|
|
64
|
+
rules={[]}
|
|
65
|
+
events={[]}
|
|
66
|
+
onCreate={onCreate}
|
|
67
|
+
onRefresh={onRefresh}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await user.click(screen.getByRole("button", { name: "New rule" }));
|
|
72
|
+
await user.type(screen.getByLabelText("Rule name *"), "Issues to Atlas");
|
|
73
|
+
await user.type(
|
|
74
|
+
screen.getByLabelText(/Trigger event type/),
|
|
75
|
+
"github.issue_assigned",
|
|
76
|
+
);
|
|
77
|
+
await user.click(screen.getByRole("button", { name: "Create rule" }));
|
|
78
|
+
|
|
79
|
+
await waitFor(() =>
|
|
80
|
+
expect(onCreate).toHaveBeenCalledWith({
|
|
81
|
+
name: "Issues to Atlas",
|
|
82
|
+
trigger_event_type: "github.issue_assigned",
|
|
83
|
+
action_type: "create_tides_task",
|
|
84
|
+
action_config: {},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
expect(onRefresh).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("includes the chosen project in the rule configuration", async () => {
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
const onCreate = vi.fn().mockResolvedValue(undefined);
|
|
93
|
+
|
|
94
|
+
render(
|
|
95
|
+
<SuiteIntegrationRules
|
|
96
|
+
canManage
|
|
97
|
+
projects={[{ id: "proj-1", name: "Atlas" }]}
|
|
98
|
+
rules={[]}
|
|
99
|
+
events={[]}
|
|
100
|
+
onCreate={onCreate}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await user.click(screen.getByRole("button", { name: "New rule" }));
|
|
105
|
+
await user.type(screen.getByLabelText("Rule name *"), "Time entries");
|
|
106
|
+
await user.type(
|
|
107
|
+
screen.getByLabelText(/Trigger event type/),
|
|
108
|
+
"googlecalendar.event_ended",
|
|
109
|
+
);
|
|
110
|
+
await user.selectOptions(
|
|
111
|
+
screen.getByLabelText("Action"),
|
|
112
|
+
"draft_time_entry",
|
|
113
|
+
);
|
|
114
|
+
await user.selectOptions(screen.getByLabelText("Project"), "proj-1");
|
|
115
|
+
await user.type(screen.getByLabelText("Minutes (optional)"), "30");
|
|
116
|
+
await user.click(screen.getByRole("button", { name: "Create rule" }));
|
|
117
|
+
|
|
118
|
+
await waitFor(() =>
|
|
119
|
+
expect(onCreate).toHaveBeenCalledWith({
|
|
120
|
+
name: "Time entries",
|
|
121
|
+
trigger_event_type: "googlecalendar.event_ended",
|
|
122
|
+
action_type: "draft_time_entry",
|
|
123
|
+
action_config: { project_id: "proj-1", minutes: "30" },
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("confirms before deleting a rule", async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
const onDelete = vi.fn().mockResolvedValue(undefined);
|
|
131
|
+
|
|
132
|
+
render(
|
|
133
|
+
<SuiteIntegrationRules
|
|
134
|
+
canManage
|
|
135
|
+
projects={[]}
|
|
136
|
+
rules={[rule]}
|
|
137
|
+
events={[]}
|
|
138
|
+
onDelete={onDelete}
|
|
139
|
+
/>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await user.click(
|
|
143
|
+
screen.getByRole("button", { name: "Delete GitHub issues to tasks" }),
|
|
144
|
+
);
|
|
145
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
146
|
+
|
|
147
|
+
const confirmation = screen.getByRole("dialog", {
|
|
148
|
+
name: "Delete automation rule",
|
|
149
|
+
});
|
|
150
|
+
expect(confirmation).toHaveTextContent("Delete GitHub issues to tasks?");
|
|
151
|
+
await user.click(
|
|
152
|
+
within(confirmation).getByRole("button", { name: "Delete rule" }),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await waitFor(() => expect(onDelete).toHaveBeenCalledWith("rule-1"));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type FormEvent } from "react";
|
|
4
|
+
import { Plus, Xmark } from "iconoir-react";
|
|
5
|
+
import {
|
|
6
|
+
Alert,
|
|
7
|
+
Badge,
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
Input,
|
|
11
|
+
Modal,
|
|
12
|
+
Select,
|
|
13
|
+
Toggle,
|
|
14
|
+
} from "@/components/ui";
|
|
15
|
+
|
|
16
|
+
export type SuiteIntegrationRuleActionType =
|
|
17
|
+
"create_tides_task" | "draft_time_entry" | "create_suite_notification";
|
|
18
|
+
|
|
19
|
+
export interface SuiteIntegrationRule {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
trigger_event_type: string;
|
|
23
|
+
action_type: string;
|
|
24
|
+
action_config: Record<string, unknown>;
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
last_fired_at: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SuiteIntegrationEvent {
|
|
30
|
+
id: string;
|
|
31
|
+
provider: string;
|
|
32
|
+
event_type: string;
|
|
33
|
+
processed_at: string | null;
|
|
34
|
+
created_at: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SuiteIntegrationRuleProject {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SuiteIntegrationRuleInput {
|
|
43
|
+
name: string;
|
|
44
|
+
trigger_event_type: string;
|
|
45
|
+
action_type: SuiteIntegrationRuleActionType;
|
|
46
|
+
action_config: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SuiteIntegrationRulesProps {
|
|
50
|
+
canManage: boolean;
|
|
51
|
+
projects: SuiteIntegrationRuleProject[];
|
|
52
|
+
rules: SuiteIntegrationRule[];
|
|
53
|
+
events: SuiteIntegrationEvent[];
|
|
54
|
+
loading?: boolean;
|
|
55
|
+
error?: string | null;
|
|
56
|
+
onCreate?: (input: SuiteIntegrationRuleInput) => Promise<void>;
|
|
57
|
+
onUpdate?: (id: string, patch: { enabled?: boolean }) => Promise<void>;
|
|
58
|
+
onDelete?: (id: string) => Promise<void>;
|
|
59
|
+
onRefresh?: () => void;
|
|
60
|
+
dataTest?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ACTION_LABELS: Record<SuiteIntegrationRuleActionType, string> = {
|
|
64
|
+
create_tides_task: "Create a Tides task",
|
|
65
|
+
draft_time_entry: "Draft a time entry",
|
|
66
|
+
create_suite_notification: "Send a suite notification",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function actionLabel(actionType: string) {
|
|
70
|
+
return (
|
|
71
|
+
ACTION_LABELS[actionType as SuiteIntegrationRuleActionType] ?? actionType
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatDateTime(value: string | null) {
|
|
76
|
+
if (!value) return "—";
|
|
77
|
+
return new Date(value).toLocaleString(undefined, {
|
|
78
|
+
dateStyle: "medium",
|
|
79
|
+
timeStyle: "short",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function SuiteIntegrationRules({
|
|
84
|
+
canManage,
|
|
85
|
+
projects,
|
|
86
|
+
rules,
|
|
87
|
+
events,
|
|
88
|
+
loading = false,
|
|
89
|
+
error = null,
|
|
90
|
+
onCreate,
|
|
91
|
+
onUpdate,
|
|
92
|
+
onDelete,
|
|
93
|
+
onRefresh,
|
|
94
|
+
dataTest = "integration-rules",
|
|
95
|
+
}: SuiteIntegrationRulesProps) {
|
|
96
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
97
|
+
const [name, setName] = useState("");
|
|
98
|
+
const [trigger, setTrigger] = useState("");
|
|
99
|
+
const [actionType, setActionType] =
|
|
100
|
+
useState<SuiteIntegrationRuleActionType>("create_tides_task");
|
|
101
|
+
const [projectId, setProjectId] = useState("");
|
|
102
|
+
const [minutes, setMinutes] = useState("");
|
|
103
|
+
const [priority, setPriority] = useState("low");
|
|
104
|
+
const [submitting, setSubmitting] = useState(false);
|
|
105
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
106
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
107
|
+
const [ruleToDelete, setRuleToDelete] =
|
|
108
|
+
useState<SuiteIntegrationRule | null>(null);
|
|
109
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
110
|
+
|
|
111
|
+
const needsProject =
|
|
112
|
+
actionType === "create_tides_task" || actionType === "draft_time_entry";
|
|
113
|
+
|
|
114
|
+
async function submitForm(event: FormEvent<HTMLFormElement>) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
if (!onCreate) return;
|
|
117
|
+
setSubmitting(true);
|
|
118
|
+
setFormError(null);
|
|
119
|
+
try {
|
|
120
|
+
const actionConfig: Record<string, string> = {};
|
|
121
|
+
if (projectId) actionConfig.project_id = projectId;
|
|
122
|
+
if (actionType === "draft_time_entry" && minutes)
|
|
123
|
+
actionConfig.minutes = minutes;
|
|
124
|
+
if (actionType === "create_tides_task" && priority !== "low")
|
|
125
|
+
actionConfig.priority = priority;
|
|
126
|
+
await onCreate({
|
|
127
|
+
name: name.trim(),
|
|
128
|
+
trigger_event_type: trigger.trim(),
|
|
129
|
+
action_type: actionType,
|
|
130
|
+
action_config: actionConfig,
|
|
131
|
+
});
|
|
132
|
+
setName("");
|
|
133
|
+
setTrigger("");
|
|
134
|
+
setProjectId("");
|
|
135
|
+
setMinutes("");
|
|
136
|
+
setPriority("low");
|
|
137
|
+
setFormOpen(false);
|
|
138
|
+
onRefresh?.();
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
setFormError(
|
|
141
|
+
cause instanceof Error ? cause.message : "Could not create the rule.",
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
setSubmitting(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function toggleRule(rule: SuiteIntegrationRule) {
|
|
149
|
+
if (!onUpdate) return;
|
|
150
|
+
setBusyId(rule.id);
|
|
151
|
+
try {
|
|
152
|
+
await onUpdate(rule.id, { enabled: !rule.enabled });
|
|
153
|
+
onRefresh?.();
|
|
154
|
+
} finally {
|
|
155
|
+
setBusyId(null);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function requestDelete(rule: SuiteIntegrationRule) {
|
|
160
|
+
setDeleteError(null);
|
|
161
|
+
setRuleToDelete(rule);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function deleteRule() {
|
|
165
|
+
if (!onDelete || !ruleToDelete) return;
|
|
166
|
+
setBusyId(ruleToDelete.id);
|
|
167
|
+
setDeleteError(null);
|
|
168
|
+
try {
|
|
169
|
+
await onDelete(ruleToDelete.id);
|
|
170
|
+
onRefresh?.();
|
|
171
|
+
setRuleToDelete(null);
|
|
172
|
+
} catch (cause) {
|
|
173
|
+
setDeleteError(
|
|
174
|
+
cause instanceof Error ? cause.message : "Could not delete the rule.",
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
setBusyId(null);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<section className="space-y-4" data-test={dataTest}>
|
|
183
|
+
<Card
|
|
184
|
+
variant="outlined"
|
|
185
|
+
bodyClassName="p-5"
|
|
186
|
+
data-test={`${dataTest}-card`}
|
|
187
|
+
>
|
|
188
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
189
|
+
<div>
|
|
190
|
+
<h3 className="font-medium text-ph-ink">Automation rules</h3>
|
|
191
|
+
<p className="mt-1 text-sm text-ph-subtle">
|
|
192
|
+
Turn incoming integration events into tasks, time entries, or
|
|
193
|
+
notifications for this workspace.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
{canManage && onCreate ? (
|
|
197
|
+
<Button
|
|
198
|
+
type="button"
|
|
199
|
+
variant="tertiary"
|
|
200
|
+
size="sm"
|
|
201
|
+
onClick={() => setFormOpen((open) => !open)}
|
|
202
|
+
data-test={`${dataTest}-new`}
|
|
203
|
+
>
|
|
204
|
+
<span className="flex items-center gap-1.5">
|
|
205
|
+
<Plus className="h-4 w-4" aria-hidden />
|
|
206
|
+
New rule
|
|
207
|
+
</span>
|
|
208
|
+
</Button>
|
|
209
|
+
) : null}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{error ? (
|
|
213
|
+
<Alert
|
|
214
|
+
status="danger"
|
|
215
|
+
title="Could not load rules"
|
|
216
|
+
description={error}
|
|
217
|
+
/>
|
|
218
|
+
) : null}
|
|
219
|
+
|
|
220
|
+
{loading ? (
|
|
221
|
+
<p className="mt-4 text-sm text-ph-subtle">Loading rules...</p>
|
|
222
|
+
) : rules.length === 0 ? (
|
|
223
|
+
<div
|
|
224
|
+
className="mt-4 rounded-lg border border-dashed border-ph-border px-4 py-6 text-center"
|
|
225
|
+
data-test={`${dataTest}-empty`}
|
|
226
|
+
>
|
|
227
|
+
<p className="text-sm text-ph-subtle">
|
|
228
|
+
No automation rules yet.
|
|
229
|
+
{canManage ? " Create one to get started." : ""}
|
|
230
|
+
</p>
|
|
231
|
+
</div>
|
|
232
|
+
) : (
|
|
233
|
+
<ul className="mt-4 space-y-2" data-test={`${dataTest}-list`}>
|
|
234
|
+
{rules.map((rule) => (
|
|
235
|
+
<li
|
|
236
|
+
key={rule.id}
|
|
237
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-ph-border bg-ph-muted px-3 py-3"
|
|
238
|
+
data-test={`${dataTest}-rule-${rule.id}`}
|
|
239
|
+
>
|
|
240
|
+
<div className="min-w-0">
|
|
241
|
+
<p className="truncate text-sm font-medium text-ph-ink">
|
|
242
|
+
{rule.name}
|
|
243
|
+
</p>
|
|
244
|
+
<p className="mt-0.5 truncate text-xs text-ph-subtle">
|
|
245
|
+
{rule.trigger_event_type} → {actionLabel(rule.action_type)}
|
|
246
|
+
</p>
|
|
247
|
+
<p className="mt-0.5 truncate text-xs text-ph-subtle">
|
|
248
|
+
Last fired: {formatDateTime(rule.last_fired_at)}
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
{canManage ? (
|
|
252
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
253
|
+
<Toggle
|
|
254
|
+
checked={rule.enabled}
|
|
255
|
+
disabled={busyId === rule.id}
|
|
256
|
+
onChange={() => void toggleRule(rule)}
|
|
257
|
+
aria-label={`Enable ${rule.name}`}
|
|
258
|
+
/>
|
|
259
|
+
<Button
|
|
260
|
+
type="button"
|
|
261
|
+
variant="tertiary"
|
|
262
|
+
size="sm"
|
|
263
|
+
disabled={busyId === rule.id}
|
|
264
|
+
onClick={() => requestDelete(rule)}
|
|
265
|
+
aria-label={`Delete ${rule.name}`}
|
|
266
|
+
>
|
|
267
|
+
<Xmark className="h-4 w-4" aria-hidden />
|
|
268
|
+
</Button>
|
|
269
|
+
</div>
|
|
270
|
+
) : (
|
|
271
|
+
<Badge
|
|
272
|
+
variant={rule.enabled ? "success" : "neutral"}
|
|
273
|
+
size="sm"
|
|
274
|
+
>
|
|
275
|
+
{rule.enabled ? "Enabled" : "Disabled"}
|
|
276
|
+
</Badge>
|
|
277
|
+
)}
|
|
278
|
+
</li>
|
|
279
|
+
))}
|
|
280
|
+
</ul>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{canManage && formOpen && onCreate ? (
|
|
284
|
+
<form
|
|
285
|
+
className="mt-4 space-y-3 rounded-lg border border-ph-border bg-ph-surface p-4"
|
|
286
|
+
onSubmit={(event) => void submitForm(event)}
|
|
287
|
+
data-test={`${dataTest}-form`}
|
|
288
|
+
>
|
|
289
|
+
{formError ? (
|
|
290
|
+
<p className="text-sm text-ph-danger" role="alert">
|
|
291
|
+
{formError}
|
|
292
|
+
</p>
|
|
293
|
+
) : null}
|
|
294
|
+
<Input
|
|
295
|
+
label="Rule name"
|
|
296
|
+
required
|
|
297
|
+
value={name}
|
|
298
|
+
onChange={(event) => setName(event.target.value)}
|
|
299
|
+
placeholder="GitHub issues to tasks"
|
|
300
|
+
data-test={`${dataTest}-name`}
|
|
301
|
+
/>
|
|
302
|
+
<Input
|
|
303
|
+
label="Trigger event type"
|
|
304
|
+
required
|
|
305
|
+
value={trigger}
|
|
306
|
+
onChange={(event) => setTrigger(event.target.value)}
|
|
307
|
+
placeholder="github.issue_assigned"
|
|
308
|
+
helperText="Provider event type, for example github.issue_assigned or googlecalendar.event_ended."
|
|
309
|
+
data-test={`${dataTest}-trigger`}
|
|
310
|
+
/>
|
|
311
|
+
<Select
|
|
312
|
+
label="Action"
|
|
313
|
+
value={actionType}
|
|
314
|
+
onChange={(event) =>
|
|
315
|
+
setActionType(
|
|
316
|
+
event.target.value as SuiteIntegrationRuleActionType,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
data-test={`${dataTest}-action`}
|
|
320
|
+
>
|
|
321
|
+
{(
|
|
322
|
+
Object.entries(ACTION_LABELS) as [
|
|
323
|
+
SuiteIntegrationRuleActionType,
|
|
324
|
+
string,
|
|
325
|
+
][]
|
|
326
|
+
).map(([value, label]) => (
|
|
327
|
+
<option key={value} value={value}>
|
|
328
|
+
{label}
|
|
329
|
+
</option>
|
|
330
|
+
))}
|
|
331
|
+
</Select>
|
|
332
|
+
{needsProject ? (
|
|
333
|
+
<Select
|
|
334
|
+
label="Project"
|
|
335
|
+
value={projectId}
|
|
336
|
+
onChange={(event) => setProjectId(event.target.value)}
|
|
337
|
+
data-test={`${dataTest}-project`}
|
|
338
|
+
>
|
|
339
|
+
<option value="">Choose a project...</option>
|
|
340
|
+
{projects.map((project) => (
|
|
341
|
+
<option key={project.id} value={project.id}>
|
|
342
|
+
{project.name}
|
|
343
|
+
</option>
|
|
344
|
+
))}
|
|
345
|
+
</Select>
|
|
346
|
+
) : null}
|
|
347
|
+
{actionType === "draft_time_entry" ? (
|
|
348
|
+
<Input
|
|
349
|
+
label="Minutes (optional)"
|
|
350
|
+
type="number"
|
|
351
|
+
min="1"
|
|
352
|
+
value={minutes}
|
|
353
|
+
onChange={(event) => setMinutes(event.target.value)}
|
|
354
|
+
data-test={`${dataTest}-minutes`}
|
|
355
|
+
/>
|
|
356
|
+
) : null}
|
|
357
|
+
{actionType === "create_tides_task" ? (
|
|
358
|
+
<Select
|
|
359
|
+
label="Priority"
|
|
360
|
+
value={priority}
|
|
361
|
+
onChange={(event) => setPriority(event.target.value)}
|
|
362
|
+
data-test={`${dataTest}-priority`}
|
|
363
|
+
>
|
|
364
|
+
<option value="low">Low</option>
|
|
365
|
+
<option value="medium">Medium</option>
|
|
366
|
+
<option value="high">High</option>
|
|
367
|
+
<option value="urgent">Urgent</option>
|
|
368
|
+
</Select>
|
|
369
|
+
) : null}
|
|
370
|
+
<div className="flex justify-end gap-2 border-t border-ph-border pt-3">
|
|
371
|
+
<Button
|
|
372
|
+
type="button"
|
|
373
|
+
variant="tertiary"
|
|
374
|
+
onClick={() => setFormOpen(false)}
|
|
375
|
+
disabled={submitting}
|
|
376
|
+
>
|
|
377
|
+
Cancel
|
|
378
|
+
</Button>
|
|
379
|
+
<Button type="submit" variant="primary" disabled={submitting}>
|
|
380
|
+
{submitting ? "Creating..." : "Create rule"}
|
|
381
|
+
</Button>
|
|
382
|
+
</div>
|
|
383
|
+
</form>
|
|
384
|
+
) : null}
|
|
385
|
+
</Card>
|
|
386
|
+
|
|
387
|
+
<Modal
|
|
388
|
+
open={ruleToDelete !== null}
|
|
389
|
+
onClose={() => {
|
|
390
|
+
if (busyId !== ruleToDelete?.id) setRuleToDelete(null);
|
|
391
|
+
}}
|
|
392
|
+
title="Delete automation rule"
|
|
393
|
+
description="This action cannot be undone."
|
|
394
|
+
dataTest={`${dataTest}-delete-confirmation`}
|
|
395
|
+
footer={
|
|
396
|
+
<div className="flex justify-end gap-2">
|
|
397
|
+
<Button
|
|
398
|
+
type="button"
|
|
399
|
+
variant="tertiary"
|
|
400
|
+
onClick={() => setRuleToDelete(null)}
|
|
401
|
+
disabled={busyId === ruleToDelete?.id}
|
|
402
|
+
>
|
|
403
|
+
Cancel
|
|
404
|
+
</Button>
|
|
405
|
+
<Button
|
|
406
|
+
type="button"
|
|
407
|
+
variant="danger"
|
|
408
|
+
onClick={() => void deleteRule()}
|
|
409
|
+
disabled={busyId === ruleToDelete?.id}
|
|
410
|
+
data-test={`${dataTest}-delete-confirm`}
|
|
411
|
+
>
|
|
412
|
+
{busyId === ruleToDelete?.id ? "Deleting..." : "Delete rule"}
|
|
413
|
+
</Button>
|
|
414
|
+
</div>
|
|
415
|
+
}
|
|
416
|
+
>
|
|
417
|
+
{deleteError ? (
|
|
418
|
+
<p className="text-sm text-ph-danger" role="alert">
|
|
419
|
+
{deleteError}
|
|
420
|
+
</p>
|
|
421
|
+
) : null}
|
|
422
|
+
<p className="text-sm text-ph-ink">
|
|
423
|
+
Delete {ruleToDelete?.name}? This cannot be undone.
|
|
424
|
+
</p>
|
|
425
|
+
</Modal>
|
|
426
|
+
|
|
427
|
+
<Card
|
|
428
|
+
variant="outlined"
|
|
429
|
+
bodyClassName="p-5"
|
|
430
|
+
data-test={`${dataTest}-events-card`}
|
|
431
|
+
>
|
|
432
|
+
<h3 className="font-medium text-ph-ink">Recent events</h3>
|
|
433
|
+
<p className="mt-1 text-sm text-ph-subtle">
|
|
434
|
+
The latest events received from your connected apps.
|
|
435
|
+
</p>
|
|
436
|
+
{events.length === 0 ? (
|
|
437
|
+
<p
|
|
438
|
+
className="mt-4 rounded-lg border border-dashed border-ph-border px-4 py-6 text-center text-sm text-ph-subtle"
|
|
439
|
+
data-test={`${dataTest}-events-empty`}
|
|
440
|
+
>
|
|
441
|
+
No integration events yet.
|
|
442
|
+
</p>
|
|
443
|
+
) : (
|
|
444
|
+
<ul className="mt-4 space-y-2" data-test={`${dataTest}-events-list`}>
|
|
445
|
+
{events.map((event) => (
|
|
446
|
+
<li
|
|
447
|
+
key={event.id}
|
|
448
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-ph-border bg-ph-muted px-3 py-2"
|
|
449
|
+
>
|
|
450
|
+
<div className="min-w-0">
|
|
451
|
+
<p className="truncate text-sm text-ph-ink">
|
|
452
|
+
{event.provider} · {event.event_type}
|
|
453
|
+
</p>
|
|
454
|
+
<p className="mt-0.5 text-xs text-ph-subtle">
|
|
455
|
+
{formatDateTime(event.created_at)}
|
|
456
|
+
</p>
|
|
457
|
+
</div>
|
|
458
|
+
<Badge
|
|
459
|
+
variant={event.processed_at ? "success" : "warning"}
|
|
460
|
+
size="sm"
|
|
461
|
+
>
|
|
462
|
+
{event.processed_at ? "Processed" : "Pending"}
|
|
463
|
+
</Badge>
|
|
464
|
+
</li>
|
|
465
|
+
))}
|
|
466
|
+
</ul>
|
|
467
|
+
)}
|
|
468
|
+
</Card>
|
|
469
|
+
</section>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cleanup, render } from "@testing-library/react";
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { SuiteMobileDrawer } from "./suite-mobile-drawer";
|
|
5
5
|
|
|
@@ -30,4 +30,20 @@ describe("SuiteMobileDrawer", () => {
|
|
|
30
30
|
);
|
|
31
31
|
expect(body?.previousElementSibling).toHaveClass("h-14", "shrink-0");
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it("uses a modal dialog that closes on Escape", () => {
|
|
35
|
+
const onClose = vi.fn();
|
|
36
|
+
render(
|
|
37
|
+
<SuiteMobileDrawer open onClose={onClose} showCloseButton>
|
|
38
|
+
<a href="/documents">Documents</a>
|
|
39
|
+
</SuiteMobileDrawer>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(screen.getByRole("dialog", { name: "Navigation" })).toHaveAttribute(
|
|
43
|
+
"aria-modal",
|
|
44
|
+
"true",
|
|
45
|
+
);
|
|
46
|
+
fireEvent.keyDown(document, { key: "Escape" });
|
|
47
|
+
expect(onClose).toHaveBeenCalledOnce();
|
|
48
|
+
});
|
|
33
49
|
});
|