Highly ambitious ATProtocol AppView service and sdks
1import { useState } from "react";
2import { graphql, useMutation } from "react-relay";
3import { useNavigate } from "react-router-dom";
4import { Dialog } from "./Dialog.tsx";
5import { FormControl } from "./FormControl.tsx";
6import { Input } from "./Input.tsx";
7import { Button } from "./Button.tsx";
8import { getRkey } from "../utils.ts";
9import type { CreateSliceDialogMutation } from "../__generated__/CreateSliceDialogMutation.graphql.ts";
10
11interface CreateSliceDialogProps {
12 open: boolean;
13 onClose: () => void;
14}
15
16export function CreateSliceDialog({ open, onClose }: CreateSliceDialogProps) {
17 const navigate = useNavigate();
18 const [name, setName] = useState("");
19 const [domain, setDomain] = useState("");
20 const [error, setError] = useState("");
21
22 const [commitMutation, isMutationInFlight] =
23 useMutation<CreateSliceDialogMutation>(
24 graphql`
25 mutation CreateSliceDialogMutation($input: NetworkSlicesSliceInput!) {
26 createNetworkSlicesSlice(input: $input) {
27 id
28 uri
29 cid
30 name
31 domain
32 createdAt
33 actorHandle
34 }
35 }
36 `
37 );
38
39 const handleSubmit = (e: React.FormEvent) => {
40 e.preventDefault();
41 setError("");
42
43 if (!name.trim()) {
44 setError("Slice name is required");
45 return;
46 }
47
48 if (!domain.trim()) {
49 setError("Domain is required");
50 return;
51 }
52
53 if (domain.toLowerCase().includes("app.bsky")) {
54 setError("app.bsky domains are not allowed");
55 return;
56 }
57
58 commitMutation({
59 variables: {
60 input: {
61 name: name.trim(),
62 domain: domain.trim(),
63 createdAt: new Date().toISOString(),
64 },
65 },
66 onCompleted: (response) => {
67 setName("");
68 setDomain("");
69 onClose();
70 // Navigate to the new slice page
71 const slice = response.createNetworkSlicesSlice;
72 const actorHandle = slice.actorHandle;
73 const rkey = getRkey(slice.uri);
74 if (actorHandle && rkey) {
75 navigate(`/profile/${actorHandle}/slice/${rkey}`);
76 } else {
77 navigate("/");
78 }
79 },
80 onError: (err) => {
81 setError(err.message || "Failed to create slice");
82 },
83 updater: (store) => {
84 const newSlice = store.getRootField("createNetworkSlicesSlice");
85 if (!newSlice) return;
86
87 const actorHandle = newSlice.getValue("actorHandle");
88 if (!actorHandle) return;
89
90 // Update Home page connection (all slices)
91 const root = store.getRoot();
92 const homeConnection = root.getLinkedRecord("networkSlicesSlices", {
93 first: 100,
94 sortBy: [{ field: "createdAt", direction: "desc" }],
95 });
96
97 if (homeConnection) {
98 const homeEdges = homeConnection.getLinkedRecords("edges") || [];
99 const newEdge = store.create(
100 `client:newEdge:${newSlice.getDataID()}`,
101 "NetworkSlicesSliceEdge"
102 );
103 newEdge.setLinkedRecord(newSlice, "node");
104 homeConnection.setLinkedRecords([newEdge, ...homeEdges], "edges");
105 }
106
107 // Update Profile page connection (filtered by actorHandle)
108 const profileConnection = root.getLinkedRecord("networkSlicesSlices", {
109 first: 100,
110 where: { actorHandle: { eq: actorHandle } },
111 sortBy: [{ field: "createdAt", direction: "desc" }],
112 });
113
114 if (profileConnection) {
115 const profileEdges = profileConnection.getLinkedRecords("edges") || [];
116 const newProfileEdge = store.create(
117 `client:newProfileEdge:${newSlice.getDataID()}`,
118 "NetworkSlicesSliceEdge"
119 );
120 newProfileEdge.setLinkedRecord(newSlice, "node");
121 profileConnection.setLinkedRecords([newProfileEdge, ...profileEdges], "edges");
122 }
123 },
124 });
125 };
126
127 const handleClose = () => {
128 setName("");
129 setDomain("");
130 setError("");
131 onClose();
132 };
133
134 return (
135 <Dialog open={open} onClose={handleClose} title="Create New Slice">
136 {error && (
137 <div className="mb-4 p-3 bg-red-900/20 border border-red-800 text-red-300 rounded text-sm">
138 {error}
139 </div>
140 )}
141
142 <form className="space-y-4" noValidate>
143 <FormControl label="Slice Name" htmlFor="name">
144 <Input
145 id="name"
146 name="name"
147 type="text"
148 value={name}
149 onChange={(e) => setName(e.target.value)}
150 placeholder="Enter slice name"
151 disabled={isMutationInFlight}
152 />
153 </FormControl>
154
155 <FormControl label="Primary Domain" htmlFor="domain">
156 <Input
157 id="domain"
158 name="domain"
159 type="text"
160 value={domain}
161 onChange={(e) => setDomain(e.target.value)}
162 placeholder="e.g. social.grain"
163 disabled={isMutationInFlight}
164 />
165 <p className="mt-1 text-xs text-zinc-500">
166 Primary namespace for this slice's collections
167 </p>
168 </FormControl>
169
170 <div className="flex justify-end gap-3 pt-4">
171 <Button
172 type="button"
173 variant="default"
174 onClick={handleClose}
175 disabled={isMutationInFlight}
176 >
177 Cancel
178 </Button>
179 <Button
180 type="button"
181 variant="primary"
182 onClick={(e) => {
183 e.preventDefault();
184 e.stopPropagation();
185 handleSubmit(e);
186 }}
187 disabled={isMutationInFlight}
188 >
189 {isMutationInFlight ? "Creating..." : "Create Slice"}
190 </Button>
191 </div>
192 </form>
193 </Dialog>
194 );
195}