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