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