Skip to content

Commit 0a6b1a7

Browse files
authored
Merge pull request #261 from codesnippetspro/soft-delete
feat: trash (soft delete)
2 parents 18d4a13 + b095be4 commit 0a6b1a7

File tree

7 files changed

+290
-51
lines changed

7 files changed

+290
-51
lines changed

src/js/components/EditorSidebar/actions/DeleteButton.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const DeleteButton: React.FC = () => {
2626

2727
<ConfirmDialog
2828
open={isDialogOpen}
29-
title={__('Permanently delete?', 'code-snippets')}
29+
title={__('Delete?', 'code-snippets')}
3030
confirmLabel={__('Delete', 'code-snippets')}
3131
confirmButtonClassName="is-destructive"
3232
onCancel={() => setIsDialogOpen(false)}
@@ -43,10 +43,9 @@ export const DeleteButton: React.FC = () => {
4343
}}
4444
>
4545
<p style={{ marginBlockStart: 0 }}>
46-
{__('You are about to permanently delete this snippet.', 'code-snippets')}{' '}
46+
{__('You are about to delete this snippet.', 'code-snippets')}{' '}
4747
{__('Are you sure?', 'code-snippets')}
4848
</p>
49-
<p><strong>{__('This action cannot be undone.', 'code-snippets')}</strong></p>
5049
</ConfirmDialog>
5150
</>
5251
)

src/php/class-list-table.php

Lines changed: 142 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class List_Table extends WP_List_Table {
3737
*
3838
* @var array<string>
3939
*/
40-
public array $statuses = [ 'all', 'active', 'inactive', 'recently_activated' ];
40+
public array $statuses = [ 'all', 'active', 'inactive', 'recently_activated', 'trashed' ];
4141

4242
/**
4343
* Column name to use when ordering the snippets list.
@@ -246,7 +246,26 @@ public function get_action_link( string $action, Snippet $snippet ): string {
246246
private function get_snippet_action_links( Snippet $snippet ): array {
247247
$actions = array();
248248

249-
if ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) {
249+
if ( $snippet->is_trashed() ) {
250+
$actions['restore'] = sprintf(
251+
'<a href="%s">%s</a>',
252+
esc_url( $this->get_action_link( 'restore', $snippet ) ),
253+
esc_html__( 'Restore', 'code-snippets' )
254+
);
255+
256+
$actions['delete_permanently'] = sprintf(
257+
'<a href="%2$s" class="delete" onclick="%3$s">%1$s</a>',
258+
esc_html__( 'Delete Permanently', 'code-snippets' ),
259+
esc_url( $this->get_action_link( 'delete_permanently', $snippet ) ),
260+
esc_js(
261+
sprintf(
262+
'return confirm("%s");',
263+
esc_html__( 'You are about to permanently delete the selected item.', 'code-snippets' ) . "\n" .
264+
esc_html__( "'Cancel' to stop, 'OK' to delete.", 'code-snippets' )
265+
)
266+
)
267+
);
268+
} elseif ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) {
250269
// Display special links if on a subsite and dealing with a network-active snippet.
251270
if ( $snippet->active ) {
252271
$actions['network_active'] = esc_html__( 'Network Active', 'code-snippets' );
@@ -267,16 +286,9 @@ private function get_snippet_action_links( Snippet $snippet ): array {
267286
}
268287

269288
$actions['delete'] = sprintf(
270-
'<a href="%2$s" class="delete" onclick="%3$s">%1$s</a>',
271-
esc_html__( 'Delete', 'code-snippets' ),
272-
esc_url( $this->get_action_link( 'delete', $snippet ) ),
273-
esc_js(
274-
sprintf(
275-
'return confirm("%s");',
276-
esc_html__( 'You are about to permanently delete the selected item.', 'code-snippets' ) . "\n" .
277-
esc_html__( "'Cancel' to stop, 'OK' to delete.", 'code-snippets' )
278-
)
279-
)
289+
'<a href="%2$s" class="delete">%1$s</a>',
290+
esc_html__( 'Trash', 'code-snippets' ),
291+
esc_url( $this->get_action_link( 'delete', $snippet ) )
280292
);
281293
}
282294

@@ -291,6 +303,10 @@ private function get_snippet_action_links( Snippet $snippet ): array {
291303
* @return string Output for activation switch.
292304
*/
293305
protected function column_activate( Snippet $snippet ): string {
306+
if ( $snippet->is_trashed() ) {
307+
return '';
308+
}
309+
294310
if ( $this->is_network && ( $snippet->shared_network || ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) ) ) {
295311
return '';
296312
}
@@ -352,8 +368,8 @@ protected function column_name( Snippet $snippet ): string {
352368

353369
$out = esc_html( $snippet->display_name );
354370

355-
// Add a link to the snippet if it isn't an unreadable network-only snippet.
356-
if ( $this->is_network || ! $snippet->network || current_user_can( code_snippets()->get_network_cap_name() ) ) {
371+
// Add a link to the snippet if it isn't an unreadable network-only snippet and isn't trashed.
372+
if ( ! $snippet->is_trashed() && ( $this->is_network || ! $snippet->network || current_user_can( code_snippets()->get_network_cap_name() ) ) ) {
357373
$out = sprintf(
358374
'<a href="%s" class="snippet-name">%s</a>',
359375
esc_attr( code_snippets()->get_snippet_edit_url( $snippet->id, $snippet->network ? 'network' : 'admin' ) ),
@@ -482,14 +498,23 @@ public function get_sortable_columns(): array {
482498
* @return array<string, string> An array of menu items with the ID paired to the label
483499
*/
484500
public function get_bulk_actions(): array {
485-
$actions = [
486-
'activate-selected' => $this->is_network ? __( 'Network Activate', 'code-snippets' ) : __( 'Activate', 'code-snippets' ),
487-
'deactivate-selected' => $this->is_network ? __( 'Network Deactivate', 'code-snippets' ) : __( 'Deactivate', 'code-snippets' ),
488-
'clone-selected' => __( 'Clone', 'code-snippets' ),
489-
'download-selected' => __( 'Export Code', 'code-snippets' ),
490-
'export-selected' => __( 'Export', 'code-snippets' ),
491-
'delete-selected' => __( 'Delete', 'code-snippets' ),
492-
];
501+
global $status;
502+
503+
if ( 'trashed' === $status ) {
504+
$actions = [
505+
'restore-selected' => __( 'Restore', 'code-snippets' ),
506+
'delete-permanently-selected' => __( 'Delete Permanently', 'code-snippets' ),
507+
];
508+
} else {
509+
$actions = [
510+
'activate-selected' => $this->is_network ? __( 'Network Activate', 'code-snippets' ) : __( 'Activate', 'code-snippets' ),
511+
'deactivate-selected' => $this->is_network ? __( 'Network Deactivate', 'code-snippets' ) : __( 'Deactivate', 'code-snippets' ),
512+
'clone-selected' => __( 'Clone', 'code-snippets' ),
513+
'download-selected' => __( 'Export Code', 'code-snippets' ),
514+
'export-selected' => __( 'Export', 'code-snippets' ),
515+
'delete-selected' => __( 'Move to Trash', 'code-snippets' ),
516+
];
517+
}
493518

494519
return apply_filters( 'code_snippets/list_table/bulk_actions', $actions );
495520
}
@@ -558,6 +583,14 @@ public function get_views(): array {
558583
'code-snippets'
559584
);
560585

586+
// translators: %s: total number of trashed snippets.
587+
$labels['trashed'] = _n(
588+
'Trashed <span class="count">(%s)</span>',
589+
'Trashed <span class="count">(%s)</span>',
590+
$count,
591+
'code-snippets'
592+
);
593+
561594
// The page URL with the status parameter.
562595
$url = esc_url( add_query_arg( 'status', $type ) );
563596

@@ -737,9 +770,17 @@ private function perform_action( int $id, string $action ) {
737770
return 'cloned';
738771

739772
case 'delete':
740-
delete_snippet( $id, $this->is_network );
773+
trash_snippet( $id, $this->is_network );
741774
return 'deleted';
742775

776+
case 'restore':
777+
restore_snippet( $id, $this->is_network );
778+
return 'restored';
779+
780+
case 'delete_permanently':
781+
delete_snippet( $id, $this->is_network );
782+
return 'deleted_permanently';
783+
743784
case 'export':
744785
$export = new Export_Attachment( [ $id ], $this->is_network );
745786
$export->download_snippets_json();
@@ -789,7 +830,28 @@ public function process_requested_actions() {
789830
$result = $this->perform_action( $id, sanitize_key( $_GET['action'] ) );
790831

791832
if ( $result ) {
792-
wp_safe_redirect( esc_url_raw( add_query_arg( 'result', $result ) ) );
833+
$redirect_args = array( 'result' => $result );
834+
835+
if ( 'deleted' === $result ) {
836+
$redirect_args['ids'] = $id;
837+
}
838+
839+
wp_safe_redirect( esc_url_raw( add_query_arg( $redirect_args ) ) );
840+
exit;
841+
}
842+
}
843+
844+
if ( isset( $_GET['action'] ) && 'restore' === $_GET['action'] && isset( $_GET['ids'] ) ) {
845+
$ids = array_map( 'intval', explode( ',', sanitize_text_field( $_GET['ids'] ) ) );
846+
847+
if ( ! empty( $ids ) ) {
848+
check_admin_referer( 'bulk-' . $this->_args['plural'] );
849+
850+
foreach ( $ids as $id ) {
851+
restore_snippet( $id, $this->is_network );
852+
}
853+
854+
wp_safe_redirect( esc_url_raw( add_query_arg( 'result', 'restored' ) ) );
793855
exit;
794856
}
795857
}
@@ -860,14 +922,35 @@ public function process_requested_actions() {
860922

861923
case 'delete-selected':
862924
foreach ( $ids as $id ) {
863-
delete_snippet( $id, $this->is_network );
925+
trash_snippet( $id, $this->is_network );
864926
}
865927
$result = 'deleted-multi';
866928
break;
929+
930+
case 'restore-selected':
931+
foreach ( $ids as $id ) {
932+
restore_snippet( $id, $this->is_network );
933+
}
934+
$result = 'restored-multi';
935+
break;
936+
937+
case 'delete-permanently-selected':
938+
foreach ( $ids as $id ) {
939+
delete_snippet( $id, $this->is_network );
940+
}
941+
$result = 'deleted-permanently-multi';
942+
break;
867943
}
868944

869945
if ( isset( $result ) ) {
870-
wp_safe_redirect( esc_url_raw( add_query_arg( 'result', $result ) ) );
946+
$redirect_args = array( 'result' => $result );
947+
948+
// Add snippet IDs for undo functionality on bulk delete
949+
if ( 'deleted-multi' === $result && ! empty( $ids ) ) {
950+
$redirect_args['ids'] = implode( ',', $ids );
951+
}
952+
953+
wp_safe_redirect( esc_url_raw( add_query_arg( $redirect_args ) ) );
871954
exit;
872955
}
873956
}
@@ -978,9 +1061,19 @@ public function prepare_items() {
9781061
$this->process_requested_actions();
9791062
$snippets = array_fill_keys( $this->statuses, array() );
9801063

981-
$snippets['all'] = apply_filters( 'code_snippets/list_table/get_snippets', get_snippets() );
1064+
$all_snippets = apply_filters( 'code_snippets/list_table/get_snippets', get_snippets() );
9821065
$this->fetch_shared_network_snippets();
9831066

1067+
// Separate trashed snippets from the main collection
1068+
$snippets['trashed'] = array_filter( $all_snippets, function( $snippet ) {
1069+
return $snippet->is_trashed();
1070+
});
1071+
1072+
// Filter out trashed snippets from the 'all' collection
1073+
$snippets['all'] = array_filter( $all_snippets, function( $snippet ) {
1074+
return ! $snippet->is_trashed();
1075+
});
1076+
9841077
foreach ( $snippets['all'] as $snippet ) {
9851078
if ( $snippet->active ) {
9861079
$this->active_by_condition[ $snippet->condition_id ][] = $snippet;
@@ -997,23 +1090,39 @@ function ( Snippet $snippet ) use ( $type ) {
9971090
return $type === $snippet->type;
9981091
}
9991092
);
1093+
1094+
// Filter trashed snippets by type
1095+
$snippets['trashed'] = array_filter(
1096+
$snippets['trashed'],
1097+
function ( Snippet $snippet ) use ( $type ) {
1098+
return $type === $snippet->type;
1099+
}
1100+
);
10001101
}
10011102

1002-
// Add scope tags.
1103+
// Add scope tags to all snippets (including trashed).
10031104
foreach ( $snippets['all'] as $snippet ) {
10041105
if ( 'global' !== $snippet->scope ) {
10051106
$snippet->add_tag( $snippet->scope );
10061107
}
10071108
}
1109+
1110+
foreach ( $snippets['trashed'] as $snippet ) {
1111+
if ( 'global' !== $snippet->scope ) {
1112+
$snippet->add_tag( $snippet->scope );
1113+
}
1114+
}
10081115

10091116
// Filter snippets by tag.
10101117
if ( ! empty( $_GET['tag'] ) ) {
10111118
$snippets['all'] = array_filter( $snippets['all'], array( $this, 'tags_filter_callback' ) );
1119+
$snippets['trashed'] = array_filter( $snippets['trashed'], array( $this, 'tags_filter_callback' ) );
10121120
}
10131121

10141122
// Filter snippets based on search query.
10151123
if ( $s ) {
10161124
$snippets['all'] = array_filter( $snippets['all'], array( $this, 'search_by_line_callback' ) );
1125+
$snippets['trashed'] = array_filter( $snippets['trashed'], array( $this, 'search_by_line_callback' ) );
10171126
}
10181127

10191128
// Clear recently activated snippets older than a week.
@@ -1037,6 +1146,11 @@ function ( Snippet $snippet ) use ( $type ) {
10371146
* @var Snippet $snippet
10381147
*/
10391148
foreach ( $snippets['all'] as $snippet ) {
1149+
// Skip trashed snippets (they're already in their own section)
1150+
if ( $snippet->is_trashed() ) {
1151+
continue;
1152+
}
1153+
10401154
if ( $snippet->active || $this->is_condition_active( $snippet ) ) {
10411155
$snippets['active'][] = $snippet;
10421156
} else {
@@ -1310,7 +1424,6 @@ public function search_notice() {
13101424
*/
13111425
public function single_row( $item ) {
13121426
$status = $item->active || $this->is_condition_active( $item ) ? 'active' : 'inactive';
1313-
13141427
$row_class = "snippet $status-snippet $item->type-snippet $item->scope-scope";
13151428

13161429
if ( $item->shared_network ) {

src/php/class-snippet.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,25 @@ class Snippet extends Data_Item {
5050
*/
5151
public const DEFAULT_DATE = '0000-00-00 00:00:00';
5252

53+
/**
54+
* Raw active value from database before processing.
55+
*
56+
* @var mixed
57+
*/
58+
private $raw_active_value;
59+
5360
/**
5461
* Constructor function.
5562
*
5663
* @param array<string, mixed>|object $initial_data Initial snippet data.
5764
*/
5865
public function __construct( $initial_data = null ) {
66+
if ( is_array( $initial_data ) && isset( $initial_data['active'] ) ) {
67+
$this->raw_active_value = $initial_data['active'];
68+
} elseif ( is_object( $initial_data ) && isset( $initial_data->active ) ) {
69+
$this->raw_active_value = $initial_data->active;
70+
}
71+
5972
$default_values = array(
6073
'id' => 0,
6174
'name' => '',
@@ -101,6 +114,15 @@ public function is_condition(): bool {
101114
return 'condition' === $this->scope;
102115
}
103116

117+
/**
118+
* Determine if the snippet is trashed (soft deleted).
119+
*
120+
* @return bool
121+
*/
122+
public function is_trashed(): bool {
123+
return -1 === (int) $this->raw_active_value;
124+
}
125+
104126
/**
105127
* Prepare a value before it is stored.
106128
*
@@ -120,7 +142,7 @@ protected function prepare_field( $value, string $field ) {
120142
return code_snippets_build_tags_array( $value );
121143

122144
case 'active':
123-
return ( is_bool( $value ) ? $value : (bool) $value ) && ! $this->is_condition();
145+
return ( is_bool( $value ) ? $value : (bool) $value ) && ! $this->is_condition() && (int) $value != -1;
124146

125147
default:
126148
return $value;

src/php/flat-files/classes/class-snippet-files.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public function register_hooks(): void {
6262
add_action( 'code_snippets/create_snippet', [ $this, 'handle_snippet' ], 10, 2 );
6363
add_action( 'code_snippets/update_snippet', [ $this, 'handle_snippet' ], 10, 2 );
6464
add_action( 'code_snippets/delete_snippet', [ $this, 'delete_snippet' ], 10, 2 );
65+
add_action( 'code_snippets/trash_snippet', [ $this, 'delete_snippet' ], 10, 2 );
6566
add_action( 'code_snippets/activate_snippet', [ $this, 'activate_snippet' ], 10, 1 );
6667
add_action( 'code_snippets/deactivate_snippet', [ $this, 'deactivate_snippet' ], 10, 2 );
6768
add_action( 'code_snippets/activate_snippets', [ $this, 'activate_snippets' ], 10, 2 );

0 commit comments

Comments
 (0)