a tool for shared writing and social publishing
at feature/thread-viewer 186 lines 5.9 kB view raw
1import { useUIState } from "src/useUIState"; 2import { 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 <div 57 className={`poll flex flex-col gap-2 p-3 w-full 58 ${isSelected ? "block-border-selected " : "block-border"}`} 59 style={{ 60 backgroundColor: 61 "color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)", 62 }} 63 > 64 <EditPollForPublication 65 entityID={props.entityID} 66 isPublished={isPublished} 67 /> 68 </div> 69 ); 70}; 71 72const EditPollForPublication = (props: { 73 entityID: string; 74 isPublished: boolean; 75}) => { 76 let pollOptions = useEntity(props.entityID, "poll/options"); 77 let { rep } = useReplicache(); 78 let permission_set = useEntitySetContext(); 79 80 return ( 81 <> 82 {props.isPublished && ( 83 <div className="text-sm italic text-tertiary"> 84 This poll has been published. You can't edit the options. 85 </div> 86 )} 87 88 {pollOptions.length === 0 && !props.isPublished && ( 89 <div className="text-center italic text-tertiary text-sm"> 90 no options yet... 91 </div> 92 )} 93 94 {pollOptions.map((p) => ( 95 <EditPollOptionForPublication 96 key={p.id} 97 entityID={p.data.value} 98 pollEntity={props.entityID} 99 disabled={props.isPublished} 100 canDelete={!props.isPublished} 101 /> 102 ))} 103 104 {!props.isPublished && permission_set.permissions.write && ( 105 <button 106 className="pollAddOption w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast" 107 onClick={async () => { 108 let pollOptionEntity = v7(); 109 await rep?.mutate.addPollOption({ 110 pollEntity: props.entityID, 111 pollOptionEntity, 112 pollOptionName: "", 113 permission_set: permission_set.set, 114 factID: v7(), 115 }); 116 117 focusElement( 118 document.getElementById( 119 elementId.block(props.entityID).pollInput(pollOptionEntity), 120 ) as HTMLInputElement | null, 121 ); 122 }} 123 > 124 Add an Option 125 </button> 126 )} 127 </> 128 ); 129}; 130 131const EditPollOptionForPublication = (props: { 132 entityID: string; 133 pollEntity: string; 134 disabled: boolean; 135 canDelete: boolean; 136}) => { 137 let { rep } = useReplicache(); 138 let { permissions } = useEntitySetContext(); 139 let optionName = useEntity(props.entityID, "poll-option/name")?.data.value; 140 141 return ( 142 <div className="flex gap-2 items-center"> 143 <AsyncValueInput 144 id={elementId.block(props.pollEntity).pollInput(props.entityID)} 145 type="text" 146 className="pollOptionInput w-full input-with-border" 147 placeholder="Option here..." 148 disabled={props.disabled || !permissions.write} 149 value={optionName || ""} 150 onChange={async (e) => { 151 await rep?.mutate.assertFact([ 152 { 153 entity: props.entityID, 154 attribute: "poll-option/name", 155 data: { type: "string", value: e.currentTarget.value }, 156 }, 157 ]); 158 }} 159 onKeyDown={(e) => { 160 if ( 161 props.canDelete && 162 e.key === "Backspace" && 163 !e.currentTarget.value 164 ) { 165 e.preventDefault(); 166 rep?.mutate.removePollOption({ optionEntity: props.entityID }); 167 } 168 }} 169 /> 170 171 {permissions.write && props.canDelete && ( 172 <button 173 tabIndex={-1} 174 className="text-accent-contrast" 175 onMouseDown={async () => { 176 await rep?.mutate.removePollOption({ 177 optionEntity: props.entityID, 178 }); 179 }} 180 > 181 <CloseTiny /> 182 </button> 183 )} 184 </div> 185 ); 186};