Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 59 additions & 8 deletions src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import React, { Fragment, useEffect, useMemo, useState } from 'react'
import classnames from 'classnames'
import { createInterpolateElement } from '@wordpress/element'
import { useRestAPI } from '../../../hooks/useRestAPI'
import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI'
import { useSnippetsList } from '../../../hooks/useSnippetsList'
import { handleUnknownError } from '../../../utils/errors'
import { downloadBulkSnippetExportFile } from '../../../utils/files'
import { REST_BASES } from '../../../utils/restAPI'
import { getSnippetDisplayName, getSnippetType } from '../../../utils/snippets/snippets'
import { cloneSnippetObject, getSnippetDisplayName, getSnippetType } from '../../../utils/snippets/snippets'
import { buildUrl } from '../../../utils/urls'
import { ListTable } from '../../common/ListTable'
import { SubmitButton } from '../../common/SubmitButton'
Expand Down Expand Up @@ -248,21 +249,59 @@ const NoItemsMessage = () => {
</>
}

const useBulkActions = (allSnippets: Snippet[]): ListTableBulkAction<Snippet['id']>[] =>
{
const useBulkActions = (allSnippets: Snippet[]): ListTableBulkAction<Snippet['id']>[] => {
const { activate, deactivate, delete: trashOrDelete, create } = useSnippetsAPI()
const { refreshSnippetsList } = useSnippetsList()

return useMemo(
() => [
{
name: __('Activate', 'code-snippets'),
apply: () => Promise.resolve()
apply: async (selected: Set<Snippet['id']>) => {
const targets = allSnippets.filter(snippet => selected.has(snippet.id) && !snippet.active)

if (0 === targets.length) {
return
}

for (const snippet of targets) {
await activate({ id: snippet.id, network: snippet.network }).catch(handleUnknownError)
}
Comment on lines +267 to +269
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written with for loops on purpose:

Run sequentially: each request mutates the flat-file config index
(php/index.php) via a non-atomic read-modify-write. Parallel calls
(Promise.all / Promise.allSettled) race on that file and corrupt it.


await refreshSnippetsList()
}
},
{
name: __('Deactivate', 'code-snippets'),
apply: () => Promise.resolve()
apply: async (selected: Set<Snippet['id']>) => {
const targets = allSnippets.filter(snippet => selected.has(snippet.id) && snippet.active)

if (0 === targets.length) {
return
}

for (const snippet of targets) {
await deactivate({ id: snippet.id, network: snippet.network }).catch(handleUnknownError)
}

await refreshSnippetsList()
}
},
{
name: __('Clone', 'code-snippets'),
apply: () => Promise.resolve()
apply: async (selected: Set<Snippet['id']>) => {
const targets = allSnippets.filter(snippet => selected.has(snippet.id) && !snippet.trashed)

if (0 === targets.length) {
return
}

for (const snippet of targets) {
await create(cloneSnippetObject(snippet)).catch(handleUnknownError)
}

await refreshSnippetsList()
}
},
{
name: __('Export', 'code-snippets'),
Expand All @@ -287,10 +326,22 @@ const useBulkActions = (allSnippets: Snippet[]): ListTableBulkAction<Snippet['id
},
{
name: __('Trash', 'code-snippets'),
apply: () => Promise.resolve()
apply: async (selected: Set<Snippet['id']>) => {
const targets = allSnippets.filter(snippet => selected.has(snippet.id))

if (0 === targets.length) {
return
}

for (const snippet of targets) {
await trashOrDelete({ id: snippet.id, network: snippet.network }).catch(handleUnknownError)
}

await refreshSnippetsList()
}
}
],
[allSnippets]
[allSnippets, activate, deactivate, trashOrDelete, create, refreshSnippetsList]
)
}

Expand Down
13 changes: 2 additions & 11 deletions src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import classnames from 'classnames'
import React, { Fragment, useState } from 'react'
import { __, sprintf } from '@wordpress/i18n'
import { __ } from '@wordpress/i18n'
import { humanTimeDiff } from '@wordpress/date'
import { RawHTML } from '@wordpress/element'
import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI'
import { useSnippetsList } from '../../../hooks/useSnippetsList'
import { handleUnknownError } from '../../../utils/errors'
import { downloadSnippetExportFile } from '../../../utils/files'
import { isNetworkAdmin } from '../../../utils/screen'
import { createSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets'
import { cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets'
import { buildUrl } from '../../../utils/urls'
import { Badge } from '../../common/Badge'
import { Button } from '../../common/Button'
Expand Down Expand Up @@ -89,15 +89,6 @@ const ActivateColumn: React.FC<ColumnProps> = ({ snippet }) => {
}
}

const cloneSnippetObject = (snippet: Snippet): Snippet =>
createSnippetObject({
...snippet,
id: 0,
active: false,
// translators: %s: snippet title.
name: sprintf(__('%s [CLONE]', 'code-snippets'), snippet.name)
})

const ActionLinks = ({ snippet }: { snippet: Snippet }) => {
const api = useSnippetsAPI()
const { refreshSnippetsList } = useSnippetsList()
Expand Down
12 changes: 10 additions & 2 deletions src/js/components/common/ListTable/ListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,14 @@ const ListTableMarkup = <T, K extends Key>({
<tbody>
<TableItems
items={visibleItems}
{...{ getKey, columns, noItems, setSelected: tableHeadingsProps.setSelected, rowClassName }}
{...{
getKey,
columns,
noItems,
selected: tableHeadingsProps.selected,
setSelected: tableHeadingsProps.setSelected,
rowClassName
}}
/>
</tbody>
<tfoot>
Expand Down Expand Up @@ -170,6 +177,7 @@ export const ListTable = <T, K extends Key>({
actions,
extraTableNav,
selected: getVisibleSelected(visibleItems, getKey, selected),
setSelected,
disabled,
currentPage,
totalPages,
Expand All @@ -178,7 +186,7 @@ export const ListTable = <T, K extends Key>({
}

const tableHeadingsProps: Omit<TableHeadingsProps<T, K>, 'which'> =
{ items: visibleItems, setSelected, columns, getKey, sortColumn, setSortColumn, sortDirection, setSortDirection }
{ items: visibleItems, selected, setSelected, columns, getKey, sortColumn, setSortColumn, sortDirection, setSortDirection }

return <ListTableMarkup
{...{
Expand Down
10 changes: 8 additions & 2 deletions src/js/components/common/ListTable/TableHeadings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Dispatch, Key, SetStateAction, ThHTMLAttributes } from 'react'
export interface TableHeadingsProps<T, K extends Key> extends Pick<ListTableProps<T, K>, 'columns' | 'getKey' | 'items'> {
which: 'head' | 'foot'
sortColumn: ListTableColumn<T> | undefined
selected: Set<K>
setSelected: Dispatch<SetStateAction<Set<K>>>
sortDirection: ListTableSortDirection
setSortColumn: Dispatch<SetStateAction<ListTableColumn<T> | undefined>>
Expand Down Expand Up @@ -72,17 +73,21 @@ export const TableHeadings = <T, K extends Key>({
getKey,
columns,
sortColumn,
selected,
setSelected,
setSortColumn,
sortDirection,
setSortDirection
}: TableHeadingsProps<T, K>) =>
<tr>
}: TableHeadingsProps<T, K>) => {
const allSelected = 0 < items.length && items.every(item => selected.has(getKey(item)))

return <tr>
<td className="column-cb check-column">
<input
id={`cb-select-all-${which}`}
type="checkbox"
name="checked[]"
checked={allSelected}
onChange={event => {
setSelected(new Set(event.target.checked ? items.map(getKey) : []))
}}
Expand Down Expand Up @@ -113,3 +118,4 @@ export const TableHeadings = <T, K extends Key>({
/>
})}
</tr>
}
9 changes: 6 additions & 3 deletions src/js/components/common/ListTable/TableItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import type { ListTableColumn, ListTableItemsProps } from './ListTable'

interface CheckboxCellProps<T, K extends Key> extends Pick<TableItemsProps<T, K>, 'getKey'> {
item: T
selected: Set<K>
setSelected: Dispatch<SetStateAction<Set<K>>>
}

const CheckboxCell = <T, K extends Key>({ item, setSelected, getKey }: CheckboxCellProps<T, K>) =>
const CheckboxCell = <T, K extends Key>({ item, selected, setSelected, getKey }: CheckboxCellProps<T, K>) =>
<th scope="row" className="check-column">
<label htmlFor={`cb-select-${getKey(item)}`}>
<span className="screen-reader-text">{__('Select snippet', 'code-snippets')}</span>
Expand All @@ -18,6 +19,7 @@ const CheckboxCell = <T, K extends Key>({ item, setSelected, getKey }: CheckboxC
id={`cb-select-${getKey(item)}`}
type="checkbox"
name="checked[]"
checked={selected.has(getKey(item))}
onChange={event => {
setSelected(previous => {
const updated = new Set(previous)
Expand Down Expand Up @@ -49,14 +51,15 @@ const TableCell = <T, >({ item, column }: TableCellProps<T>) => {

export interface TableItemsProps<T, K extends Key>
extends Pick<ListTableItemsProps<T, K>, 'items' | 'getKey' | 'columns' | 'noItems' | 'rowClassName'> {
selected: Set<K>
setSelected: Dispatch<SetStateAction<Set<K>>>
}

export const TableItems = <T, K extends Key>({ items, getKey, columns, noItems, setSelected, rowClassName }: TableItemsProps<T, K>) =>
export const TableItems = <T, K extends Key>({ items, getKey, columns, noItems, selected, setSelected, rowClassName }: TableItemsProps<T, K>) =>
0 < items.length
? items.map(item =>
<tr key={getKey(item)} className={rowClassName?.(item)}>
<CheckboxCell {...{ item, setSelected, getKey }} />
<CheckboxCell {...{ item, selected, setSelected, getKey }} />

{columns.map(column =>
<TableCell key={column.id} item={item} column={column} />)}
Expand Down
15 changes: 14 additions & 1 deletion src/js/components/common/ListTable/TableNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,17 @@ const BulkActionSelect = <K extends Key,>({

interface BulkActionsProps<K extends Key> extends Required<Pick<TableNavProps<K>, 'which' | 'actions'>> {
applyAction: (action: ListTableBulkAction<K>) => Promise<void>
onActionSuccess?: () => void
disabled?: boolean
}

const BulkActions = function BulkActions<K extends Key>({ which, actions, applyAction, disabled }: BulkActionsProps<K>) {
const BulkActions = function BulkActions<K extends Key>({
which,
actions,
applyAction,
onActionSuccess,
disabled
}: BulkActionsProps<K>) {
const [selectedAction, setSelectedAction] = useState<ListTableBulkAction<K>>()
const [selectedActionName, setSelectedActionName] = useState('-1')
const [isPerformingAction, setIsPerformingAction] = useState(false)
Expand Down Expand Up @@ -85,6 +92,9 @@ const BulkActions = function BulkActions<K extends Key>({ which, actions, applyA
if (selectedAction) {
setIsPerformingAction(true)
applyAction(selectedAction)
.then(() => {
onActionSuccess?.()
})
.catch(handleUnknownError)
.finally(() => {
setIsPerformingAction(false)
Expand All @@ -101,6 +111,7 @@ const BulkActions = function BulkActions<K extends Key>({ which, actions, applyA
export interface TableNavProps<K extends Key> extends ListTableNavProps<K>, Omit<TablePaginationProps, 'totalPages'> {
which: 'top' | 'bottom'
selected: Set<K>
setSelected: Dispatch<SetStateAction<Set<K>>>
totalItems: number
totalPages: number | undefined
}
Expand All @@ -109,6 +120,7 @@ export const TableNav = <K extends Key,>({
which,
actions,
selected,
setSelected,
totalItems,
totalPages = 0,
extraTableNav,
Expand All @@ -123,6 +135,7 @@ export const TableNav = <K extends Key,>({
actions={actions}
disabled={paginationProps.disabled}
applyAction={action => action.apply(selected)}
onActionSuccess={() => setSelected(new Set())}
/>)}

{extraTableNav?.(which)}
Expand Down
9 changes: 9 additions & 0 deletions src/js/utils/snippets/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ const PRO_TYPES = new Set<SnippetType>(['css', 'js', 'cond'])
export const createSnippetObject = (fields: unknown): Snippet =>
parseSnippetObject(fields)

export const cloneSnippetObject = (snippet: Snippet): Snippet =>
createSnippetObject({
...snippet,
id: 0,
active: false,
// translators: %s: snippet title.
name: sprintf(__('%s [CLONE]', 'code-snippets'), snippet.name)
})

export const getSnippetType = ({ scope }: Pick<Snippet, 'scope'>): SnippetType => {
switch (true) {
case scope.endsWith('-css'):
Expand Down
Loading