a tool for shared writing and social publishing
at update/thread-viewer 191 lines 6.0 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 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};