a tool for shared writing and social publishing
1import { useUIState } from "src/useUIState";
2import { BlockLayout, BlockProps } from "./Block";
3import { useMemo } from "react";
4import { AsyncValueInput } from "components/Input";
5import { focusElement } from "src/utils/focusElement";
6import { useEntitySetContext } from "components/EntitySetProvider";
7import { useEntity, useReplicache } from "src/replicache";
8import { v7 } from "uuid";
9import { elementId } from "src/utils/elementId";
10import { CloseTiny } from "components/Icons/CloseTiny";
11import { useLeafletPublicationData } from "components/PageSWRDataProvider";
12import {
13 PubLeafletBlocksPoll,
14 PubLeafletPagesLinearDocument,
15} from "lexicons/api";
16import { getDocumentPages } from "src/utils/normalizeRecords";
17import { ids } from "lexicons/api/lexicons";
18
19/**
20 * PublicationPollBlock is used for editing polls in publication documents.
21 * It allows adding/editing options when the poll hasn't been published yet,
22 * but disables adding new options once the poll record exists (indicated by pollUri).
23 */
24export const PublicationPollBlock = (
25 props: BlockProps & {
26 areYouSure?: boolean;
27 setAreYouSure?: (value: boolean) => void;
28 },
29) => {
30 let { data: publicationData, normalizedDocument } =
31 useLeafletPublicationData();
32 let isSelected = useUIState((s) =>
33 s.selectedBlocks.find((b) => b.value === props.entityID),
34 );
35 // Check if this poll has been published in a publication document
36 const isPublished = useMemo(() => {
37 if (!normalizedDocument) return false;
38
39 const pages = getDocumentPages(normalizedDocument);
40 if (!pages) return false;
41
42 // Search through all pages and blocks to find if this poll entity has been published
43 for (const page of pages) {
44 if (page.$type === "pub.leaflet.pages.linearDocument") {
45 const linearPage = page as PubLeafletPagesLinearDocument.Main;
46 for (const blockWrapper of linearPage.blocks || []) {
47 if (blockWrapper.block?.$type === ids.PubLeafletBlocksPoll) {
48 const pollBlock = blockWrapper.block as PubLeafletBlocksPoll.Main;
49 // Check if this poll's rkey matches our entity ID
50 const rkey = pollBlock.pollRef.uri.split("/").pop();
51 if (rkey === props.entityID) {
52 return true;
53 }
54 }
55 }
56 }
57 }
58 return false;
59 }, [normalizedDocument, props.entityID]);
60
61 return (
62 <BlockLayout
63 className="poll flex flex-col gap-2"
64 hasBackground={"accent"}
65 isSelected={!!isSelected}
66 areYouSure={props.areYouSure}
67 setAreYouSure={props.setAreYouSure}
68 >
69 <EditPollForPublication
70 entityID={props.entityID}
71 isPublished={isPublished}
72 />
73 </BlockLayout>
74 );
75};
76
77const EditPollForPublication = (props: {
78 entityID: string;
79 isPublished: boolean;
80}) => {
81 let pollOptions = useEntity(props.entityID, "poll/options");
82 let { rep } = useReplicache();
83 let permission_set = useEntitySetContext();
84
85 return (
86 <>
87 {props.isPublished && (
88 <div className="text-sm italic text-tertiary">
89 This poll has been published. You can't edit the options.
90 </div>
91 )}
92
93 {pollOptions.length === 0 && !props.isPublished && (
94 <div className="text-center italic text-tertiary text-sm">
95 no options yet...
96 </div>
97 )}
98
99 {pollOptions.map((p) => (
100 <EditPollOptionForPublication
101 key={p.id}
102 entityID={p.data.value}
103 pollEntity={props.entityID}
104 disabled={props.isPublished}
105 canDelete={!props.isPublished}
106 />
107 ))}
108
109 {!props.isPublished && permission_set.permissions.write && (
110 <button
111 className="pollAddOption w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast"
112 onClick={async () => {
113 let pollOptionEntity = v7();
114 await rep?.mutate.addPollOption({
115 pollEntity: props.entityID,
116 pollOptionEntity,
117 pollOptionName: "",
118 permission_set: permission_set.set,
119 factID: v7(),
120 });
121
122 focusElement(
123 document.getElementById(
124 elementId.block(props.entityID).pollInput(pollOptionEntity),
125 ) as HTMLInputElement | null,
126 );
127 }}
128 >
129 Add an Option
130 </button>
131 )}
132 </>
133 );
134};
135
136const EditPollOptionForPublication = (props: {
137 entityID: string;
138 pollEntity: string;
139 disabled: boolean;
140 canDelete: boolean;
141}) => {
142 let { rep } = useReplicache();
143 let { permissions } = useEntitySetContext();
144 let optionName = useEntity(props.entityID, "poll-option/name")?.data.value;
145
146 return (
147 <div className="flex gap-2 items-center">
148 <AsyncValueInput
149 id={elementId.block(props.pollEntity).pollInput(props.entityID)}
150 type="text"
151 className="pollOptionInput w-full input-with-border"
152 placeholder="Option here..."
153 disabled={props.disabled || !permissions.write}
154 value={optionName || ""}
155 onChange={async (e) => {
156 await rep?.mutate.assertFact([
157 {
158 entity: props.entityID,
159 attribute: "poll-option/name",
160 data: { type: "string", value: e.currentTarget.value },
161 },
162 ]);
163 }}
164 onKeyDown={(e) => {
165 if (
166 props.canDelete &&
167 e.key === "Backspace" &&
168 !e.currentTarget.value
169 ) {
170 e.preventDefault();
171 rep?.mutate.removePollOption({ optionEntity: props.entityID });
172 }
173 }}
174 />
175
176 {permissions.write && props.canDelete && (
177 <button
178 tabIndex={-1}
179 className="text-accent-contrast"
180 onMouseDown={async () => {
181 await rep?.mutate.removePollOption({
182 optionEntity: props.entityID,
183 });
184 }}
185 >
186 <CloseTiny />
187 </button>
188 )}
189 </div>
190 );
191};