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