| 1 |
import classnames from 'classnames' |
| 2 |
import { humanTimeDiff } from '@wordpress/date' |
| 3 |
import { RawHTML } from '@wordpress/element' |
| 4 |
import { __, sprintf } from '@wordpress/i18n' |
| 5 |
import React, { useState } from 'react' |
| 6 |
import { ConfirmDeleteDialog, useDeleteSnippet } from '../../common/snippets/ConfirmDeleteDialog' |
| 7 |
import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI' |
| 8 |
import { useSnippetsList } from '../../../hooks/useSnippetsList' |
| 9 |
import { handleUnknownError } from '../../../utils/errors' |
| 10 |
import { downloadSnippetExportFile } from '../../../utils/files' |
| 11 |
import { canModifySnippet, cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType, isNetworkOnlySnippet, isSnippetActive } from '../../../utils/snippets/snippets' |
| 12 |
import { Button } from '../../common/Button' |
| 13 |
import { KebabMenu, KebabMenuDivider, KebabMenuItem, KebabMenuRow } from '../../common/KebabMenu' |
| 14 |
import { SnippetCard } from '../../common/snippets/SnippetCard' |
| 15 |
import { SnippetPreviewModal } from '../../common/snippets/SnippetPreviewModal' |
| 16 |
import { useFilteredSnippets } from './WithFilteredSnippetsContext' |
| 17 |
import { ActivateColumn, PriorityColumn, SnippetExtraIcons, SnippetName, TagsColumn, TypeColumn } from './TableColumns' |
| 18 |
import type { Snippet } from '../../../types/Snippet' |
| 19 |
|
| 20 |
interface SnippetCardActionsProps { |
| 21 |
snippet: Snippet |
| 22 |
} |
| 23 |
|
| 24 |
interface SnippetCardActionsProps { |
| 25 |
snippet: Snippet |
| 26 |
} |
| 27 |
|
| 28 |
const CardPreviewButton: React.FC<SnippetCardActionsProps> = ({ snippet }) => { |
| 29 |
const [isPreviewOpen, setIsPreviewOpen] = useState(false) |
| 30 |
|
| 31 |
return ( |
| 32 |
<> |
| 33 |
<Button secondary onClick={() => setIsPreviewOpen(true)}> |
| 34 |
{__('Preview', 'code-snippets')} |
| 35 |
</Button> |
| 36 |
|
| 37 |
{isPreviewOpen && ( |
| 38 |
<SnippetPreviewModal |
| 39 |
snippet={snippet} |
| 40 |
setIsOpen={setIsPreviewOpen} |
| 41 |
/>)} |
| 42 |
</> |
| 43 |
) |
| 44 |
} |
| 45 |
|
| 46 |
const CloneExportMenuItems: React.FC<SnippetCardActionsProps> = ({ snippet }) => { |
| 47 |
const api = useSnippetsAPI() |
| 48 |
const { refreshSnippetsList } = useSnippetsList() |
| 49 |
|
| 50 |
return ( |
| 51 |
<> |
| 52 |
<KebabMenuItem |
| 53 |
onSelect={() => { |
| 54 |
api.create(cloneSnippetObject(snippet)) |
| 55 |
.then(refreshSnippetsList) |
| 56 |
.catch(handleUnknownError) |
| 57 |
}} |
| 58 |
> |
| 59 |
{__('Clone', 'code-snippets')} |
| 60 |
</KebabMenuItem> |
| 61 |
|
| 62 |
<KebabMenuItem |
| 63 |
onSelect={() => { |
| 64 |
api.export(snippet) |
| 65 |
.then(response => downloadSnippetExportFile(response, snippet)) |
| 66 |
.catch(handleUnknownError) |
| 67 |
}} |
| 68 |
> |
| 69 |
{__('Export', 'code-snippets')} |
| 70 |
</KebabMenuItem> |
| 71 |
|
| 72 |
<KebabMenuDivider /> |
| 73 |
</> |
| 74 |
) |
| 75 |
} |
| 76 |
|
| 77 |
interface RestoreDeleteMenuItemsProps extends SnippetCardActionsProps { |
| 78 |
requestDelete: VoidFunction |
| 79 |
} |
| 80 |
|
| 81 |
const RestoreDeleteMenuItems: React.FC<RestoreDeleteMenuItemsProps> = ({ |
| 82 |
snippet, |
| 83 |
requestDelete |
| 84 |
}) => { |
| 85 |
const api = useSnippetsAPI() |
| 86 |
const { refreshSnippetsList } = useSnippetsList() |
| 87 |
|
| 88 |
return ( |
| 89 |
<> |
| 90 |
<KebabMenuDivider /> |
| 91 |
|
| 92 |
{snippet.trashed |
| 93 |
? <KebabMenuItem |
| 94 |
onSelect={() => { |
| 95 |
api.restore(snippet) |
| 96 |
.then(refreshSnippetsList) |
| 97 |
.catch(handleUnknownError) |
| 98 |
}} |
| 99 |
> |
| 100 |
{__('Restore', 'code-snippets')} |
| 101 |
</KebabMenuItem> |
| 102 |
: null} |
| 103 |
|
| 104 |
<KebabMenuItem destructive onSelect={requestDelete}> |
| 105 |
{snippet.trashed ? __('Delete Permanently', 'code-snippets') : __('Trash', 'code-snippets')} |
| 106 |
</KebabMenuItem> |
| 107 |
</> |
| 108 |
) |
| 109 |
} |
| 110 |
|
| 111 |
const CardActionsMenu: React.FC<SnippetCardActionsProps> = ({ snippet }) => { |
| 112 |
const { refreshSnippetsList } = useSnippetsList() |
| 113 |
|
| 114 |
const { requestDelete, deleteDialogProps } = useDeleteSnippet({ |
| 115 |
snippet, |
| 116 |
onSuccess: refreshSnippetsList, |
| 117 |
onError: handleUnknownError |
| 118 |
}) |
| 119 |
|
| 120 |
return ( |
| 121 |
<> |
| 122 |
<KebabMenu |
| 123 |
label={sprintf( |
| 124 |
/* translators: %s: name of the snippet. */ |
| 125 |
__('Actions for %s', 'code-snippets'), |
| 126 |
getSnippetDisplayName(snippet) |
| 127 |
)} |
| 128 |
> |
| 129 |
{!snippet.trashed ? <CloneExportMenuItems snippet={snippet} /> : null} |
| 130 |
|
| 131 |
<KebabMenuRow className="kebab-menu-priority"> |
| 132 |
<label htmlFor={`snippet-${snippet.id}-priority`}>{__('Priority', 'code-snippets')}</label> |
| 133 |
<PriorityColumn snippet={snippet} /> |
| 134 |
</KebabMenuRow> |
| 135 |
|
| 136 |
{canModifySnippet(snippet) && (snippet.trashed || !snippet.locked) && ( |
| 137 |
<RestoreDeleteMenuItems snippet={snippet} requestDelete={() => void requestDelete()} />)} |
| 138 |
</KebabMenu> |
| 139 |
|
| 140 |
<ConfirmDeleteDialog {...deleteDialogProps} /> |
| 141 |
</> |
| 142 |
) |
| 143 |
} |
| 144 |
|
| 145 |
const CardFooterActions: React.FC<SnippetCardActionsProps> = ({ snippet }) => |
| 146 |
<> |
| 147 |
{isNetworkOnlySnippet(snippet) |
| 148 |
? snippet.active |
| 149 |
? <span className="network-active">{__('Network Active', 'code-snippets')}</span> |
| 150 |
: <span className="network-only">{__('Network Only', 'code-snippets')}</span> |
| 151 |
: null} |
| 152 |
|
| 153 |
{!snippet.trashed ? <CardPreviewButton snippet={snippet} /> : null} |
| 154 |
|
| 155 |
{!snippet.trashed && canModifySnippet(snippet) |
| 156 |
? <a className="button button-primary" href={getSnippetEditUrl(snippet)}> |
| 157 |
{snippet.locked ? __('View', 'code-snippets') : __('Edit', 'code-snippets')} |
| 158 |
</a> |
| 159 |
: null} |
| 160 |
|
| 161 |
{canModifySnippet(snippet) ? <CardActionsMenu snippet={snippet} /> : null} |
| 162 |
</> |
| 163 |
|
| 164 |
const CardModifiedDate: React.FC<SnippetCardActionsProps> = ({ snippet }) => |
| 165 |
snippet.modified |
| 166 |
? <time className="snippet-card-modified" dateTime={snippet.modified} title={snippet.modified}> |
| 167 |
{sprintf( |
| 168 |
/* translators: %s: human-readable time difference, including "ago" suffix. */ |
| 169 |
__('Modified %s', 'code-snippets'), |
| 170 |
humanTimeDiff(snippet.modified, undefined) |
| 171 |
)} |
| 172 |
</time> |
| 173 |
: null |
| 174 |
|
| 175 |
const CardHeader: React.FC<SnippetCardActionsProps> = ({ snippet }) => |
| 176 |
<div className="snippet-card-header"> |
| 177 |
<ActivateColumn snippet={snippet} /> |
| 178 |
<TypeColumn snippet={snippet} /> |
| 179 |
<h3><SnippetName snippet={snippet} /></h3> |
| 180 |
<SnippetExtraIcons snippet={snippet} /> |
| 181 |
</div> |
| 182 |
|
| 183 |
export interface ManageSnippetCardProps { |
| 184 |
snippet: Snippet |
| 185 |
isSelected: boolean |
| 186 |
onSelectedChange: (isSelected: boolean) => void |
| 187 |
} |
| 188 |
|
| 189 |
export const ManageSnippetCard: React.FC<ManageSnippetCardProps> = ({ |
| 190 |
snippet, |
| 191 |
isSelected, |
| 192 |
onSelectedChange |
| 193 |
}) => { |
| 194 |
const { activeByCondition } = useFilteredSnippets() |
| 195 |
|
| 196 |
return ( |
| 197 |
<SnippetCard |
| 198 |
className={classnames( |
| 199 |
'snippet', |
| 200 |
`${isSnippetActive(snippet, activeByCondition) ? 'active' : 'inactive'}-snippet`, |
| 201 |
`${getSnippetType(snippet)}-snippet`, |
| 202 |
`${snippet.scope}-snippet` |
| 203 |
)} |
| 204 |
isSelected={isSelected} |
| 205 |
onSelectedChange={onSelectedChange} |
| 206 |
selectionLabel={sprintf( |
| 207 |
/* translators: %s: name of the snippet. */ |
| 208 |
__('Select %s', 'code-snippets'), |
| 209 |
getSnippetDisplayName(snippet) |
| 210 |
)} |
| 211 |
footer={<CardFooterActions snippet={snippet} />} |
| 212 |
> |
| 213 |
<div className="card-inner"> |
| 214 |
<CardHeader snippet={snippet} /> |
| 215 |
|
| 216 |
{(0 < snippet.tags.length || !!snippet.modified) && ( |
| 217 |
<div className={classnames('snippet-card-meta', { 'has-tags': 0 < snippet.tags.length })}> |
| 218 |
{0 < snippet.tags.length && ( |
| 219 |
<span className="snippet-card-tags"> |
| 220 |
<span className="snippet-card-tags-label"> |
| 221 |
{__('Tags:', 'code-snippets')} |
| 222 |
</span> <TagsColumn snippet={snippet} /> |
| 223 |
</span>)} |
| 224 |
|
| 225 |
<CardModifiedDate snippet={snippet} /> |
| 226 |
</div>)} |
| 227 |
|
| 228 |
{snippet.desc && ( |
| 229 |
<div className="snippet-description-content"><RawHTML>{snippet.desc}</RawHTML></div>)} |
| 230 |
</div> |
| 231 |
</SnippetCard> |
| 232 |
) |
| 233 |
} |
| 234 |
|