Description.tsx123 lines · main
| 1 | import { ident, literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format' |
| 2 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 3 | import { noop } from 'lodash' |
| 4 | import { Loader } from 'lucide-react' |
| 5 | import { useState } from 'react' |
| 6 | import { toast } from 'sonner' |
| 7 | import { Button, ExpandingTextArea } from 'ui' |
| 8 | |
| 9 | import { executeSql } from '@/data/sql/execute-sql-query' |
| 10 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 11 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 12 | import { timeout } from '@/lib/helpers' |
| 13 | |
| 14 | // Removes some auto-generated Postgrest text |
| 15 | // Ideally PostgREST wouldn't add this if there is already a comment |
| 16 | const temp_removePostgrestText = (content: string) => { |
| 17 | const postgrestTextPk = `Note:\nThis is a Primary Key.<pk/>` |
| 18 | const postgrestTextFk = `Note:\nThis is a Foreign Key to` |
| 19 | const pkTextPos = content.lastIndexOf(postgrestTextPk) |
| 20 | const fkTextPos = content.lastIndexOf(postgrestTextFk) |
| 21 | |
| 22 | let cleansed = content |
| 23 | if (pkTextPos >= 0) cleansed = cleansed.substring(0, pkTextPos) |
| 24 | if (fkTextPos >= 0) cleansed = cleansed.substring(0, fkTextPos) |
| 25 | return cleansed |
| 26 | } |
| 27 | |
| 28 | interface DescrptionProps { |
| 29 | content: string |
| 30 | metadata: { table?: string; column?: string; rpc?: string } |
| 31 | onChange: (value: string) => void |
| 32 | } |
| 33 | |
| 34 | const Description = ({ content, metadata, onChange = noop }: DescrptionProps) => { |
| 35 | const contentText = temp_removePostgrestText(content || '').trim() |
| 36 | const [value, setValue] = useState(contentText) |
| 37 | const [isUpdating, setIsUpdating] = useState(false) |
| 38 | const { data: project } = useSelectedProjectQuery() |
| 39 | |
| 40 | const { table, column, rpc } = metadata |
| 41 | |
| 42 | const hasChanged = value != contentText |
| 43 | const animateCss = `transition duration-150` |
| 44 | |
| 45 | const { can: canUpdateDescription } = useAsyncCheckPermissions( |
| 46 | PermissionAction.TENANT_SQL_QUERY, |
| 47 | '*' |
| 48 | ) |
| 49 | |
| 50 | const updateDescription = async () => { |
| 51 | if (isUpdating || !canUpdateDescription) return false |
| 52 | |
| 53 | setIsUpdating(true) |
| 54 | let query: SafeSqlFragment | undefined |
| 55 | if (table && column) |
| 56 | query = safeSql`comment on column ${ident('public')}.${ident(table)}.${ident(column)} is ${literal(value)};` |
| 57 | if (table && !column) |
| 58 | query = safeSql`comment on table ${ident('public')}.${ident(table)} is ${literal(value)};` |
| 59 | if (rpc) query = safeSql`comment on function ${ident(rpc)} is ${literal(value)};` |
| 60 | |
| 61 | if (query) { |
| 62 | try { |
| 63 | await executeSql({ |
| 64 | projectRef: project?.ref, |
| 65 | connectionString: project?.connectionString, |
| 66 | sql: query, |
| 67 | }) |
| 68 | // [Joshen] Temp fix, immediately refreshing the docs fetches stale state |
| 69 | await timeout(500) |
| 70 | toast.success(`Successfully updated description`) |
| 71 | } catch (error: any) { |
| 72 | toast.error(`Failed to update description: ${error.message}`) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | onChange(value) |
| 77 | setIsUpdating(false) |
| 78 | } |
| 79 | |
| 80 | if (!canUpdateDescription) { |
| 81 | return ( |
| 82 | <span className={`block text-sm ${value ? 'text-foreground' : ''}`}> |
| 83 | {value || 'No description'} |
| 84 | </span> |
| 85 | ) |
| 86 | } |
| 87 | |
| 88 | return ( |
| 89 | <div className="space-y-2 px-0.5"> |
| 90 | <ExpandingTextArea |
| 91 | className="w-full min-h-auto" |
| 92 | placeholder="Click to edit." |
| 93 | value={value} |
| 94 | onChange={(e: any) => setValue(e.target.value)} |
| 95 | /> |
| 96 | <div |
| 97 | className={`flex items-center gap-2 ${ |
| 98 | hasChanged ? 'opacity-100' : 'h-0 cursor-default opacity-0' |
| 99 | } ${animateCss}`} |
| 100 | > |
| 101 | <Button |
| 102 | type="default" |
| 103 | disabled={!hasChanged} |
| 104 | onClick={() => { |
| 105 | setValue(contentText) |
| 106 | setIsUpdating(false) |
| 107 | }} |
| 108 | > |
| 109 | Cancel |
| 110 | </Button> |
| 111 | <Button disabled={!hasChanged} onClick={updateDescription}> |
| 112 | {isUpdating ? ( |
| 113 | <Loader className="mx-auto animate-spin" size={14} strokeWidth={2} /> |
| 114 | ) : ( |
| 115 | <span>Save</span> |
| 116 | )} |
| 117 | </Button> |
| 118 | </div> |
| 119 | </div> |
| 120 | ) |
| 121 | } |
| 122 | |
| 123 | export default Description |