Skip to content

Commit 807fc36

Browse files
authored
Merge pull request #2017 from maxberger/master
Implement Share by map, improve share by token, added test action for javascript errors.
2 parents 0404e54 + 555c973 commit 807fc36

File tree

16 files changed

+1330
-787
lines changed

16 files changed

+1330
-787
lines changed

.github/workflows/test.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,29 @@ jobs:
186186
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
187187
run: coveralls --service=github --finish
188188

189+
js-test:
190+
name: JS Type Check
191+
runs-on: ubuntu-latest
192+
needs: test-ubuntu-python-newest
193+
steps:
194+
- uses: actions/checkout@v4
195+
- uses: actions/setup-node@v4
196+
with:
197+
node-version: 24
198+
- name: JS Type Check
199+
run: |
200+
set -o pipefail
201+
npx -p typescript tsc -p radicale/web/jsconfig.json | npx typescript-xunit-xml > tsc-results.xml
202+
- uses: mikepenz/action-junit-report@v6
203+
if: ${{ failure() && (github.event.pull_request.head.repo.full_name != github.repository) }}
204+
with:
205+
report_paths: 'tsc-results.xml'
206+
annotate_only: true # forked repo cannot write to checks so just do annotations
207+
- uses: mikepenz/action-junit-report@v6
208+
if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository }}
209+
with:
210+
report_paths: 'tsc-results.xml'
211+
189212
lint:
190213
name: Lint
191214
runs-on: ubuntu-latest
@@ -220,6 +243,6 @@ jobs:
220243
report_paths: 'pytest-results.xml'
221244
annotate_only: true # forked repo cannot write to checks so just do annotations
222245
- uses: mikepenz/action-junit-report@v6
223-
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
246+
if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository }}
224247
with:
225248
report_paths: 'pytest-results.xml'

integ_tests/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def start_radicale_server(tmp_path: pathlib.Path) -> Generator[str, Any, None]:
4545
with open(user_path, "w") as f:
4646
f.write(
4747
"""admin:adminpassword
48+
max:maxpassword
49+
4850
"""
4951
)
5052

integ_tests/test_sharing.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non
2222
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
2323
).to_have_count(0)
2424

25-
page.click('button[data-name="sharebytoken_ro"]')
25+
page.click('button[data-name="sharebytoken"]')
26+
page.click('#newshare button[data-name="submit"]')
2627
expect(
2728
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
2829
).to_have_count(1)
@@ -34,7 +35,9 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non
3435
expect(
3536
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
3637
).to_have_count(0)
37-
page.click('button[data-name="sharebytoken_rw"]')
38+
page.click('button[data-name="sharebytoken"]')
39+
page.click('label[for="newshare_attr_permissions_rw"]')
40+
page.click('#newshare button[data-name="submit"]')
3841
expect(
3942
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
4043
).to_have_count(1)
@@ -46,3 +49,46 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non
4649
expect(
4750
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
4851
).to_have_count(0)
52+
53+
54+
def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> None:
55+
login(page, radicale_server)
56+
create_collection(page, radicale_server)
57+
page.hover("article:not(.hidden)")
58+
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
59+
60+
expect(
61+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
62+
).to_have_count(0)
63+
64+
page.click('button[data-name="sharebymap"]')
65+
page.locator('input[data-name="shareuser"]').fill("max")
66+
page.locator('input[data-name="sharehref"]').fill("1234")
67+
page.click('#newshare button[data-name="submit"]')
68+
expect(
69+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
70+
).to_have_count(1)
71+
expect(
72+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) img[alt='RO']")
73+
).to_be_visible()
74+
page.once("dialog", lambda dialog: dialog.accept())
75+
page.click('tr:not(.hidden) button[data-name="delete"]', strict=True)
76+
expect(
77+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
78+
).to_have_count(0)
79+
page.click('button[data-name="sharebymap"]')
80+
page.click('label[for="newshare_attr_permissions_rw"]')
81+
page.locator('input[data-name="shareuser"]').fill("max")
82+
page.locator('input[data-name="sharehref"]').fill("1234")
83+
page.click('#newshare button[data-name="submit"]')
84+
expect(
85+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
86+
).to_have_count(1)
87+
expect(
88+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) img[alt='RW']")
89+
).to_be_visible()
90+
page.once("dialog", lambda dialog: dialog.accept())
91+
page.click('tr:not(.hidden) button[data-name="delete"]', strict=True)
92+
expect(
93+
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
94+
).to_have_count(0)

radicale/web/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Built-in web UI
2+
3+
If you have tsc installed, you can type-check all JavaScript using
4+
5+
``` lang=shell
6+
tsc -p radicale/web/jsconfig.json --noEmit --pretty
7+
```
8+

radicale/web/internal_data/CollectionsScene.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import { CreateEditCollectionScene } from "./CreateEditCollectionScene.js";
2222
import { DeleteCollectionScene } from "./DeleteCollectionScene.js";
2323
import { LoadingScene } from "./LoadingScene.js";
24-
import { CreateShareCollectionScene, maybe_enable_sharing_options } from "./ShareCollectionScene.js";
24+
import { ShareCollectionScene, maybe_enable_sharing_options } from "./ShareCollectionScene.js";
2525
import { UploadCollectionScene } from "./UploadCollectionScene.js";
2626
import { discover_server_features, get_collections } from "./api.js";
2727
import { SERVER } from "./constants.js";
@@ -30,15 +30,16 @@ import { Scene, pop_scene, push_scene, scene_stack } from "./scene_manager.js";
3030
import { bytesToHumanReadable } from "./utils.js";
3131

3232
/**
33-
* @constructor
3433
* @implements {Scene}
35-
* @param {string} user
36-
* @param {string} password
37-
* @param {Collection} collection The principal collection.
38-
* @param {function(string):void} onerror Called when an error occurs, before the
39-
* scene is popped.
4034
*/
4135
export class CollectionsScene {
36+
/**
37+
* @param {string} user
38+
* @param {string} password
39+
* @param {Collection} collection The collection to show sharing options for.
40+
* @param {function(string):void} onerror Called when an error occurs, before the
41+
* scene is popped.
42+
*/
4243
constructor(user, password, collection, onerror) {
4344
/** @type {HTMLElement} */ let html_scene = document.getElementById("collectionsscene");
4445
/** @type {HTMLElement} */ let template = html_scene.querySelector("[data-name=collectiontemplate]");
@@ -82,7 +83,7 @@ export class CollectionsScene {
8283

8384
function onshare(collection) {
8485
try {
85-
let share_collection_scene = new CreateShareCollectionScene(user, password, collection);
86+
let share_collection_scene = new ShareCollectionScene(user, password, collection);
8687
push_scene(share_collection_scene, false);
8788
} catch (err) {
8889
console.error(err);
@@ -189,7 +190,7 @@ export class CollectionsScene {
189190
upload_btn.onclick = onupload;
190191
if (collections === null) {
191192
update();
192-
discover_server_features(user, password, maybe_enable_sharing_options);
193+
discover_server_features(user, password, maybe_enable_sharing_options);
193194
} else {
194195
// from update loading scene
195196
show_collections(collections);

radicale/web/internal_data/CreateEditCollectionScene.js

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,31 @@ import { create_collection, edit_collection } from "./api.js";
2323
import { COLOR_RE } from "./constants.js";
2424
import { Collection, CollectionType } from "./models.js";
2525
import { Scene, pop_scene, push_scene, scene_stack } from "./scene_manager.js";
26-
import { cleanHREFinput, isValidHREF, random_hex, random_uuid } from "./utils.js";
26+
import { cleanHREFinput, isValidHREF, onCleanHREFinput, random_hex, random_uuid } from "./utils.js";
2727

2828
/**
29-
* @constructor
3029
* @implements {Scene}
31-
* @param {string} user
32-
* @param {string} password
33-
* @param {Collection} collection if it's a principal collection, a new
34-
* collection will be created inside of it.
35-
* Otherwise the collection will be edited.
3630
*/
3731
export class CreateEditCollectionScene {
32+
/**
33+
* @param {string} user
34+
* @param {string} password
35+
* @param {Collection} collection if it's a principal collection, a new
36+
* collection will be created inside of it.
37+
* Otherwise the collection will be edited.
38+
*/
3839
constructor(user, password, collection) {
3940
let edit = collection.type !== CollectionType.PRINCIPAL;
4041
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
4142
/** @type {HTMLElement} */ let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
4243
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
4344
/** @type {HTMLInputElement} */ let href_form = html_scene.querySelector("[data-name=href]");
44-
/** @type {HTMLElement} */ let href_label = html_scene.querySelector("label[for=href]");
4545
/** @type {HTMLInputElement} */ let displayname_form = html_scene.querySelector("[data-name=displayname]");
46-
/** @type {HTMLElement} */ let displayname_label = html_scene.querySelector("label[for=displayname]");
4746
/** @type {HTMLInputElement} */ let description_form = html_scene.querySelector("[data-name=description]");
48-
/** @type {HTMLElement} */ let description_label = html_scene.querySelector("label[for=description]");
4947
/** @type {HTMLInputElement} */ let source_form = html_scene.querySelector("[data-name=source]");
5048
/** @type {HTMLElement} */ let source_label = html_scene.querySelector("label[for=source]");
5149
/** @type {HTMLSelectElement} */ let type_form = html_scene.querySelector("[data-name=type]");
52-
/** @type {HTMLElement} */ let type_label = html_scene.querySelector("label[for=type]");
5350
/** @type {HTMLInputElement} */ let color_form = html_scene.querySelector("[data-name=color]");
54-
/** @type {HTMLElement} */ let color_label = html_scene.querySelector("label[for=color]");
5551
/** @type {HTMLElement} */ let submit_btn = html_scene.querySelector("[data-name=submit]");
5652
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
5753

@@ -69,7 +65,7 @@ export class CreateEditCollectionScene {
6965
let color = edit && collection.color ? collection.color : "#" + random_hex(6);
7066

7167
if (!edit) {
72-
href_form.addEventListener("keydown", cleanHREFinput);
68+
href_form.addEventListener("input", onCleanHREFinput);
7369
}
7470

7571
function remove_invalid_types() {
@@ -118,7 +114,7 @@ export class CreateEditCollectionScene {
118114
error_form.classList.remove("hidden");
119115
}
120116
error_form.classList.add("hidden");
121-
onTypeChange();
117+
onTypeChange(null);
122118
type_form.addEventListener("change", onTypeChange);
123119
}
124120

@@ -172,8 +168,10 @@ export class CreateEditCollectionScene {
172168
return false;
173169
}
174170

175-
176-
function onTypeChange(e) {
171+
/**
172+
* @param {Event} _e
173+
*/
174+
function onTypeChange(_e) {
177175
if (type_form.value == CollectionType.WEBCAL) {
178176
source_label.classList.remove("hidden");
179177
source_form.classList.remove("hidden");

radicale/web/internal_data/LoadingScene.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@
2121
import { Scene } from "./scene_manager.js";
2222

2323
/**
24-
* @constructor
2524
* @implements {Scene}
2625
*/
27-
export function LoadingScene() {
28-
let html_scene = document.getElementById("loadingscene");
29-
this.show = function() {
30-
html_scene.classList.remove("hidden");
31-
};
32-
this.hide = function() {
33-
html_scene.classList.add("hidden");
34-
};
35-
this.release = function() {};
26+
export class LoadingScene {
27+
constructor() {
28+
let html_scene = document.getElementById("loadingscene");
29+
this.show = function () {
30+
html_scene.classList.remove("hidden");
31+
};
32+
this.hide = function () {
33+
html_scene.classList.add("hidden");
34+
};
35+
this.release = function () { };
36+
}
3637
}

0 commit comments

Comments
 (0)