Skip to content

Commit e7ed08e

Browse files
Implement auto-recover functionality [B: 1403] (#483)
1 parent 29bbae0 commit e7ed08e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2494
-284
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ jobs:
178178

179179
strategy:
180180
matrix:
181-
node-version: [18.x, 20.x]
181+
node-version: [18.x, 20.x, 22.x]
182182
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
183183

184184
steps:
@@ -187,6 +187,7 @@ jobs:
187187
uses: actions/setup-node@v4
188188
with:
189189
node-version: ${{ matrix.node-version }}
190-
cache: 'yarn'
190+
cache: 'npm'
191+
- run: npx update-browserslist-db@latest
191192
- run: yarn
192193
- run: yarn jest --passWithNoTests

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ const config = {
9494
"^validation$": "<rootDir>/src/frontend/js/lib/validation",
9595
"^logging$": "<rootDir>/src/frontend/js/lib/logging",
9696
"^util/(.*)$": "<rootDir>/src/frontend/js/lib/util/$1",
97+
"^components/(.*)$": "<rootDir>/src/frontend/components/$1",
98+
"^set-field-values$": "<rootDir>/src/frontend/js/lib/set-field-values",
99+
"^guid$": "<rootDir>/src/frontend/js/lib/guid",
97100
},
98101

99102
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader

lib/GADS.pm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4761,7 +4761,8 @@ sub _process_edit
47614761
{
47624762
# The "source" parameter is user input, make sure still valid
47634763
my $source_curval = $layout->column(param('source'), permission => 'read');
4764-
try { $record->write(dry_run => 1, parent_curval => $source_curval) };
4764+
my %options = (dry_run => 1, parent_curval => $source_curval);
4765+
try { $record->write(%options) };
47654766
if (my $e = $@->wasFatal)
47664767
{
47674768
push @validation_errors, $e->reason eq 'PANIC' ? 'An unexpected error occurred' : $e->message;
@@ -4885,6 +4886,9 @@ sub _process_edit
48854886
$params->{content_block_custom_classes} = 'content-block--footer';
48864887
}
48874888

4889+
$params->{clone_from} = $clone_from
4890+
if $clone_from;
4891+
48884892
$params->{modal_field_ids} = encode_json $layout->column($modal)->curval_field_ids
48894893
if $modal;
48904894

lib/GADS/API.pm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,17 @@ any ['get', 'post'] => '/api/users' => require_any_role [qw/useradmin superadmin
14481448
return encode_json $return;
14491449
};
14501450

1451+
get '/api/get_key' => require_login sub {
1452+
my $user = logged_in_user;
1453+
1454+
my $key = $user->encryption_key;
1455+
1456+
return to_json {
1457+
error => 0,
1458+
key => $key
1459+
}
1460+
};
1461+
14511462
sub _success
14521463
{ my $msg = shift;
14531464
send_as JSON => {

lib/GADS/Role/Presentation/Column/Curcommon.pm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package GADS::Role::Presentation::Column::Curcommon;
22

3+
use JSON qw(encode_json);
34
use Moo::Role;
45

56
sub after_presentation
@@ -12,6 +13,7 @@ sub after_presentation
1213
$return->{data_filter_fields} = $self->data_filter_fields;
1314
$return->{typeahead_use_id} = 1;
1415
$return->{limit_rows} = $self->limit_rows;
16+
$return->{modal_field_ids} = encode_json $self->curval_field_ids;
1517
# Expensive to build, so avoid if possible. Only needed for an edit, and no
1618
# point if they are filtered from record values as they will be rebuilt
1719
# anyway

lib/GADS/Schema/Result/User.pm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use GADS::Config;
1616
use GADS::Email;
1717
use HTML::Entities qw/encode_entities/;
1818
use Log::Report;
19+
use MIME::Base64 qw/encode_base64url/;
20+
use Digest::SHA qw/hmac_sha256 sha256/;
1921
use Moo;
2022

2123
extends 'DBIx::Class::Core';
@@ -1249,4 +1251,23 @@ sub export_hash
12491251
};
12501252
}
12511253

1254+
has encryption_key => (
1255+
is => 'lazy',
1256+
);
1257+
1258+
sub _build_encryption_key {
1259+
my $self = shift;
1260+
1261+
my $header_json = '{"typ":"JWT","alg":"HS256"}';
1262+
# This is a string because encode_json created the JSON string in an arbitrary order and we need the same key _every time_!
1263+
my $payload_json = '{"sub":"' . $self->id . '","user":"' . $self->username . '"}';
1264+
my $header_b64 = encode_base64url($header_json);
1265+
my $payload_b64 = encode_base64url($payload_json);
1266+
my $input = "$header_b64.$payload_b64";
1267+
my $secret = sha256($self->password);
1268+
my $sig = encode_base64url(hmac_sha256($input, $secret));
1269+
1270+
return encode_base64url(sha256("$input.$sig"));
1271+
}
1272+
12521273
1;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { clearSavedFormValues } from "./common";
2+
3+
export default function createCancelButton(el: HTMLElement | JQuery<HTMLElement>) {
4+
const $el = $(el);
5+
if ($el[0].tagName !== 'BUTTON') return;
6+
$el.data('cancel-button', "true");
7+
$el.on('click', async () => {
8+
const href = $el.data('href');
9+
await clearSavedFormValues($el.closest('form'));
10+
if (href)
11+
window.location.href = href;
12+
else
13+
window.history.back();
14+
});
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import "../../../testing/globals.definitions";
2+
import {layoutId, recordId, table_key} from "./common";
3+
4+
describe("Common button tests",()=>{
5+
it("should populate table_key",()=>{
6+
expect(table_key()).toBe("linkspace-record-change-undefined-0"); // Undefined because $('body').data('layout-identifier') is not defined
7+
});
8+
9+
it("should have a layoutId", ()=>{
10+
$('body').data('layout-identifier', 'layoutId');
11+
expect(layoutId()).toBe('layoutId');
12+
});
13+
14+
it("should have a recordId", ()=>{
15+
expect(isNaN(parseInt(location.pathname.split('/').pop() ?? ""))).toBe(true);
16+
expect(recordId()).toBe(0);
17+
});
18+
19+
it("should populate table_key fully",()=>{
20+
$('body').data('layout-identifier', 'layoutId');
21+
expect(table_key()).toBe("linkspace-record-change-layoutId-0");
22+
});
23+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import gadsStorage from "util/gadsStorage";
2+
3+
/**
4+
* Clear all saved form values for the current record
5+
* @param $form The form to clear the data for
6+
*/
7+
export async function clearSavedFormValues($form: JQuery<HTMLElement>) {
8+
if (!$form || $form.length === 0) return;
9+
const layout = layoutId();
10+
const record = recordId();
11+
const ls = storage();
12+
const item = await ls.getItem(table_key());
13+
14+
if (item) ls.removeItem(`linkspace-record-change-${layout}-${record}`);
15+
await Promise.all($form.find(".linkspace-field").map(async (_, el) => {
16+
const field_id = $(el).data("column-id");
17+
const item = await gadsStorage.getItem(`linkspace-column-${field_id}-${layout}-${record}`);
18+
if (item) gadsStorage.removeItem(`linkspace-column-${field_id}-${layout}-${record}`);
19+
}));
20+
}
21+
22+
/**
23+
* Get the layout identifier from the body data
24+
* @returns The layout identifier
25+
*/
26+
export function layoutId() {
27+
return $('body').data('layout-identifier');
28+
}
29+
30+
/**
31+
* Get the record identifier from the body data
32+
* @returns The record identifier
33+
*/
34+
export function recordId() {
35+
return $('body').find('.form-edit').data('current-id') || 0;
36+
}
37+
38+
/**
39+
* Get the key for the table used for saving form values
40+
* @returns The key for the table
41+
*/
42+
export function table_key() {
43+
return `linkspace-record-change-${layoutId()}-${recordId()}`;
44+
}
45+
46+
/**
47+
* Get the storage object - this originally was used in debugging to allow for the storage object to be mocked
48+
* @returns The storage object
49+
*/
50+
export function storage() {
51+
return gadsStorage;
52+
}

src/frontend/components/button/lib/component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ class ButtonComponent extends Component {
105105
createRemoveUnloadButton(el);
106106
});
107107
});
108+
map.set('btn-js-cancel', (el) => {
109+
import(/* webpackChunkName: "cancel-button" */ './cancel-button')
110+
.then(({default: createCancelButton}) => {
111+
createCancelButton(el);
112+
});
113+
});
108114
ButtonComponent.staticButtonsMap = map;
109115
}
110116

0 commit comments

Comments
 (0)