Merge pull request #2030 from maxberger/master
Consistency updates, security updates, and support for birthday sharing
This commit is contained in:
@@ -60,6 +60,8 @@ collection_by_token = true
|
||||
permit_create_token = true
|
||||
permit_create_map = true
|
||||
permit_properties_overlay = true
|
||||
collection_by_bday = true
|
||||
permit_create_bday = true
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -41,18 +41,18 @@ def test_delete_wrong_confirmation(page: Page, radicale_server: str) -> None:
|
||||
page.click('article:not(.hidden) a[data-name="delete"]', force=True)
|
||||
|
||||
# Input wrong confirmation
|
||||
page.fill('#deletecollectionscene input[data-name="confirmationtxt"]', "foo")
|
||||
page.click('#deletecollectionscene button[data-name="delete"]')
|
||||
page.fill('#deleteconfirmationscene input[data-name="confirmationtxt"]', "foo")
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
|
||||
# Check for error message
|
||||
error_locator = page.locator('#deletecollectionscene span[data-name="error"]')
|
||||
error_locator = page.locator('#deleteconfirmationscene span[data-name="error"]')
|
||||
expect(error_locator).to_be_visible()
|
||||
expect(error_locator).to_contain_text(
|
||||
"Please type DELETE in the confirmation field"
|
||||
)
|
||||
|
||||
# Scene should still be visible
|
||||
expect(page.locator("#deletecollectionscene")).to_be_visible()
|
||||
expect(page.locator("#deleteconfirmationscene")).to_be_visible()
|
||||
|
||||
|
||||
def test_delete_correct_confirmation(page: Page, radicale_server: str) -> None:
|
||||
@@ -67,11 +67,11 @@ def test_delete_correct_confirmation(page: Page, radicale_server: str) -> None:
|
||||
page.click('article:not(.hidden) a[data-name="delete"]', force=True)
|
||||
|
||||
# Input correct confirmation
|
||||
page.fill('#deletecollectionscene input[data-name="confirmationtxt"]', "DELETE")
|
||||
page.click('#deletecollectionscene button[data-name="delete"]')
|
||||
page.fill('#deleteconfirmationscene input[data-name="confirmationtxt"]', "DELETE")
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
|
||||
# Verify collection is gone
|
||||
expect(page.locator("article:not(.hidden)")).to_have_count(0)
|
||||
|
||||
# Scene should be hidden
|
||||
expect(page.locator("#deletecollectionscene")).to_be_hidden()
|
||||
expect(page.locator("#deleteconfirmationscene")).to_be_hidden()
|
||||
|
||||
@@ -68,10 +68,10 @@ def test_navigation_delete_collection_cancel(page: Page, radicale_server: str) -
|
||||
|
||||
page.hover("article:not(.hidden)")
|
||||
page.click('article:not(.hidden) a[data-name="delete"]', force=True)
|
||||
expect(page.locator("#deletecollectionscene")).to_be_visible()
|
||||
expect(page.locator("#deleteconfirmationscene")).to_be_visible()
|
||||
|
||||
page.click('#deletecollectionscene button[data-name="cancel"]')
|
||||
expect(page.locator("#deletecollectionscene")).to_be_hidden()
|
||||
page.click('#deleteconfirmationscene button[data-name="cancel"]')
|
||||
expect(page.locator("#deleteconfirmationscene")).to_be_hidden()
|
||||
expect(page.locator("#collectionsscene")).to_be_visible()
|
||||
|
||||
|
||||
@@ -82,18 +82,18 @@ def test_navigation_delete_collection_confirm(page: Page, radicale_server: str)
|
||||
|
||||
page.hover("article:not(.hidden)")
|
||||
page.click('article:not(.hidden) a[data-name="delete"]', force=True)
|
||||
expect(page.locator("#deletecollectionscene")).to_be_visible()
|
||||
expect(page.locator("#deleteconfirmationscene")).to_be_visible()
|
||||
|
||||
# We need to fill the confirmation text
|
||||
confirmation_text = page.locator(
|
||||
"#deletecollectionscene [data-name='deleteconfirmationtext']"
|
||||
"#deleteconfirmationscene [data-name='deleteconfirmationtext']"
|
||||
).inner_text()
|
||||
page.locator("#deletecollectionscene input[data-name='confirmationtxt']").fill(
|
||||
page.locator("#deleteconfirmationscene input[data-name='confirmationtxt']").fill(
|
||||
confirmation_text
|
||||
)
|
||||
page.click('#deletecollectionscene button[data-name="delete"]')
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
|
||||
expect(page.locator("#deletecollectionscene")).to_be_hidden()
|
||||
expect(page.locator("#deleteconfirmationscene")).to_be_hidden()
|
||||
expect(page.locator("#collectionsscene")).to_be_visible()
|
||||
expect(page.locator("article:not(.hidden)")).to_have_count(0)
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non
|
||||
"tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='ro']"
|
||||
)
|
||||
).to_be_visible()
|
||||
page.once("dialog", lambda dialog: dialog.accept())
|
||||
page.click('tr:not(.hidden) button[data-name="delete"]', strict=True)
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
expect(
|
||||
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
|
||||
).to_have_count(0)
|
||||
@@ -68,8 +68,8 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non
|
||||
"tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='rw']"
|
||||
)
|
||||
).to_be_visible()
|
||||
page.once("dialog", lambda dialog: dialog.accept())
|
||||
page.click('tr:not(.hidden) button[data-name="delete"]', strict=True)
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
expect(
|
||||
page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)")
|
||||
).to_have_count(0)
|
||||
@@ -97,8 +97,8 @@ def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> Non
|
||||
"tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='ro']"
|
||||
)
|
||||
).to_be_visible()
|
||||
page.once("dialog", lambda dialog: dialog.accept())
|
||||
page.click('tr:not(.hidden) button[data-name="delete"]', strict=True)
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
expect(
|
||||
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
|
||||
).to_have_count(0)
|
||||
@@ -115,8 +115,8 @@ def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> Non
|
||||
"tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='rw']"
|
||||
)
|
||||
).to_be_visible()
|
||||
page.once("dialog", lambda dialog: dialog.accept())
|
||||
page.click('tr:not(.hidden) button[data-name="delete"]', strict=True)
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
expect(
|
||||
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
|
||||
).to_have_count(0)
|
||||
@@ -139,15 +139,29 @@ def test_share_with_property_overrides(page: Page, radicale_server: str) -> None
|
||||
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
|
||||
page.click('button[data-name="sharebytoken"]')
|
||||
|
||||
# Verify property override is closed by default
|
||||
expect(
|
||||
page.locator('input[data-name="displayname_override_enabled"]')
|
||||
).not_to_be_visible()
|
||||
page.click('details[data-name="properties_override"] summary')
|
||||
|
||||
# Verify defaults
|
||||
expect(page.locator('input[data-name="displayname_override"]')).to_have_value(
|
||||
"Test Collection"
|
||||
)
|
||||
expect(page.locator('input[data-name="description_override"]')).to_have_value(
|
||||
"Original Description"
|
||||
)
|
||||
expect(page.locator('input[data-name="color_override"]')).to_have_value("#ff0000")
|
||||
expect(page.locator('input[data-name="displayname_override"]')).to_be_disabled()
|
||||
expect(page.locator('input[data-name="description_override"]')).to_be_disabled()
|
||||
expect(page.locator('input[data-name="color_override"]')).to_be_disabled()
|
||||
|
||||
# Set overrides
|
||||
page.click('label[for="newshare_attr_displayname_enabled"]')
|
||||
page.locator('input[data-name="displayname_override"]').fill(
|
||||
"Overridden Displayname"
|
||||
)
|
||||
page.click('label[for="newshare_attr_description_enabled"]')
|
||||
page.locator('input[data-name="description_override"]').fill(
|
||||
"Overridden Description"
|
||||
@@ -182,8 +196,20 @@ def test_share_journal_no_overrides(page: Page, radicale_server: str) -> None:
|
||||
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
|
||||
page.click('button[data-name="sharebytoken"]')
|
||||
|
||||
# Verify property override fieldset is hidden
|
||||
expect(page.locator('fieldset[data-name="properties_override"]')).to_be_hidden()
|
||||
# Verify property override visibility
|
||||
expect(page.locator('details[data-name="properties_override"]')).to_be_visible()
|
||||
expect(
|
||||
page.locator('input[data-name="displayname_override_enabled"]')
|
||||
).not_to_be_visible()
|
||||
page.click('details[data-name="properties_override"] summary')
|
||||
|
||||
expect(
|
||||
page.locator('input[data-name="displayname_override_enabled"]')
|
||||
).to_be_visible()
|
||||
expect(
|
||||
page.locator('input[data-name="description_override_enabled"]')
|
||||
).to_be_hidden()
|
||||
expect(page.locator('input[data-name="color_override_enabled"]')).to_be_hidden()
|
||||
|
||||
# Create the share
|
||||
page.click('#newshare button[data-name="submit"]')
|
||||
@@ -423,3 +449,107 @@ def test_no_incoming_shares_message(page: Page, radicale_server: str) -> None:
|
||||
|
||||
page.click('#incomingsharingscene button[data-name="cancel"]')
|
||||
expect(page.locator("#incomingsharingscene")).to_be_hidden()
|
||||
|
||||
|
||||
def test_create_and_delete_share_by_bday(page: Page, radicale_server: str) -> None:
|
||||
login(page, radicale_server)
|
||||
# create collection of type ADDRESSBOOK for bday (bday only works with ADDRESSBOOK)
|
||||
page.click('a[data-name="new"]')
|
||||
page.locator('#createcollectionscene select[data-name="type"]').select_option(
|
||||
"ADDRESSBOOK"
|
||||
)
|
||||
page.locator('#createcollectionscene input[data-name="displayname"]').fill(
|
||||
"Addressbook For Bday"
|
||||
)
|
||||
page.click('#createcollectionscene button[data-name="submit"]')
|
||||
|
||||
page.hover("article:not(.hidden)")
|
||||
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
|
||||
|
||||
expect(
|
||||
page.locator("tr[data-name='sharebdayrowtemplate']:not(.hidden)")
|
||||
).to_have_count(0)
|
||||
|
||||
page.click('button[data-name="sharebybday"]')
|
||||
|
||||
# verify user is auto-filled with current user (admin)
|
||||
expect(page.locator('input[data-name="shareuser"]')).to_have_value("admin")
|
||||
page.locator('input[data-name="sharehref"]').fill("bdaymapped")
|
||||
|
||||
# verify that the permissions section is hidden entirely
|
||||
expect(page.locator("input#newshare_attr_permissions_ro")).to_be_hidden()
|
||||
expect(page.locator("input#newshare_attr_permissions_rw")).to_be_hidden()
|
||||
|
||||
page.click('#newshare button[data-name="submit"]')
|
||||
expect(
|
||||
page.locator("tr[data-name='sharebdayrowtemplate']:not(.hidden)")
|
||||
).to_have_count(1)
|
||||
|
||||
# verify no permissions pill in the bday row
|
||||
expect(
|
||||
page.locator(
|
||||
"tr[data-name='sharebdayrowtemplate']:not(.hidden) span[data-name='ro']"
|
||||
)
|
||||
).to_have_count(0)
|
||||
|
||||
# Close the share scene and verify the virtual bday calendar is now in the collections list
|
||||
page.click('#sharecollectionscene button[data-name="cancel"]')
|
||||
expect(page.locator("#sharecollectionscene")).to_be_hidden()
|
||||
|
||||
# The virtual calendar (bdaymapped) should appear as its own article
|
||||
# after the cache was invalidated following the self-share
|
||||
expect(page.locator("article:not(.hidden)")).to_have_count(2)
|
||||
|
||||
# Delete the bday share by re-opening the share scene
|
||||
page.hover("article:not(.hidden) >> nth=0")
|
||||
page.click('article:not(.hidden) >> nth=0 >> a[data-name="share"]', force=True)
|
||||
page.click(
|
||||
"tr[data-name='sharebdayrowtemplate']:not(.hidden) button[data-name='delete']",
|
||||
strict=True,
|
||||
)
|
||||
page.click('#deleteconfirmationscene button[data-name="delete"]')
|
||||
expect(
|
||||
page.locator("tr[data-name='sharebdayrowtemplate']:not(.hidden)")
|
||||
).to_have_count(0)
|
||||
|
||||
|
||||
def test_bday_section_hidden_for_calendar(page: Page, radicale_server: str) -> None:
|
||||
"""Verify the bday calendar section is hidden for CALENDAR collections."""
|
||||
login(page, radicale_server)
|
||||
|
||||
page.click('a[data-name="new"]')
|
||||
page.locator('#createcollectionscene select[data-name="type"]').select_option(
|
||||
"CALENDAR"
|
||||
)
|
||||
page.locator('#createcollectionscene input[data-name="displayname"]').fill(
|
||||
"My Calendar"
|
||||
)
|
||||
page.click('#createcollectionscene button[data-name="submit"]')
|
||||
|
||||
page.hover("article:not(.hidden)")
|
||||
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
|
||||
|
||||
expect(page.locator("#sharecollectionscene")).to_be_visible()
|
||||
expect(page.locator("div[data-name='sharebybday']")).to_be_hidden()
|
||||
page.click('#sharecollectionscene button[data-name="cancel"]')
|
||||
|
||||
|
||||
def test_bday_section_visible_for_addressbook(page: Page, radicale_server: str) -> None:
|
||||
"""Verify the bday calendar section is visible for ADDRESSBOOK collections."""
|
||||
login(page, radicale_server)
|
||||
|
||||
page.click('a[data-name="new"]')
|
||||
page.locator('#createcollectionscene select[data-name="type"]').select_option(
|
||||
"ADDRESSBOOK"
|
||||
)
|
||||
page.locator('#createcollectionscene input[data-name="displayname"]').fill(
|
||||
"My Addressbook"
|
||||
)
|
||||
page.click('#createcollectionscene button[data-name="submit"]')
|
||||
|
||||
page.hover("article:not(.hidden)")
|
||||
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
|
||||
|
||||
expect(page.locator("#sharecollectionscene")).to_be_visible()
|
||||
expect(page.locator("div[data-name='sharebybday']")).to_be_visible()
|
||||
page.click('#sharecollectionscene button[data-name="cancel"]')
|
||||
|
||||
@@ -198,7 +198,10 @@ def _serve_traversable(
|
||||
return NOT_FOUND
|
||||
content_type = MIMETYPES.get(
|
||||
os.path.splitext(traversable.name)[1].lower(), FALLBACK_MIMETYPE)
|
||||
headers = {"Content-Type": content_type}
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"Content-Security-Policy": "default-src 'self'; object-src 'none'"
|
||||
}
|
||||
if isinstance(traversable, pathlib.Path):
|
||||
headers["Last-Modified"] = time.strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT",
|
||||
|
||||
@@ -112,6 +112,7 @@ main {
|
||||
#logoutview span {
|
||||
width: calc(100% - 60px);
|
||||
display: inline-block;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#logoutview a {
|
||||
@@ -191,6 +192,7 @@ main {
|
||||
font-size: 1em;
|
||||
max-height: 130px;
|
||||
overflow: overlay;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#collectionsscene article:hover ul {
|
||||
@@ -518,3 +520,7 @@ button.inline {
|
||||
margin: 0 2px;
|
||||
width: 1.4em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -14,17 +14,12 @@
|
||||
<title>Radicale Web Interface</title>
|
||||
<link href="css/main.css" type="text/css" media="screen" rel="stylesheet">
|
||||
<link href="css/icon.png" type="image/png" rel="icon">
|
||||
<style>
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav id="logoutview" class="hidden">
|
||||
<span data-name="user" style="word-wrap:break-word;"></span>
|
||||
<span data-name="user"></span>
|
||||
<a href="#" class="green" data-name="refresh" title="Refresh">Refresh</a>
|
||||
<a href="#" class="red" data-name="logout" title="Logout">Logout</a>
|
||||
</nav>
|
||||
@@ -75,6 +70,7 @@
|
||||
<h3 class="title" data-name="title">Title</h3>
|
||||
<small>
|
||||
<span data-name="shared-by" class="hidden">👥 shared by <span data-name="shared-by-owner"></span> ></span>
|
||||
<span data-name="transformed-from" class="hidden">Address book ⟿</span>
|
||||
<span data-name="ADDRESSBOOK">Address book</span>
|
||||
<span data-name="CALENDAR_JOURNAL_TASKS">Calendar, journal and tasks</span>
|
||||
<span data-name="CALENDAR_JOURNAL">Calendar and journal</span>
|
||||
@@ -86,8 +82,8 @@
|
||||
<span data-name="WEBCAL">Webcal</span>
|
||||
</small>
|
||||
<small data-name="contentcount"></small>
|
||||
<input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);">
|
||||
<p data-name="description" style="word-wrap:break-word;">Description</p>
|
||||
<input type="text" data-name="url" value="" readonly="" class="selectall">
|
||||
<p data-name="description">Description</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="" title="Download" class="green" data-name="download">
|
||||
@@ -152,56 +148,85 @@
|
||||
<p>Manage sharing for collection <span class="title" data-name="title">title</span>
|
||||
</p>
|
||||
<div data-name="sharebytoken">
|
||||
<h2>By Token</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr data-name="sharetokenrowtemplate" class="hidden">
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="edit"><img src="css/icons/edit.svg"
|
||||
class="small_icon" alt="Edit"></button>
|
||||
</td>
|
||||
<td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td>
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly=""
|
||||
onfocus="this.setSelectionRange(0, 99999);" class="inline"></td>
|
||||
<td>
|
||||
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
|
||||
class="small_icon" alt="Delete"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="button" class="blue inline" data-name="sharebytoken"><img src="css/icons/new.svg"
|
||||
class="small_icon" alt="New Share by Token"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<details open>
|
||||
<summary>By Token</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr data-name="sharetokenrowtemplate" class="hidden">
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="edit"><img src="css/icons/edit.svg"
|
||||
class="small_icon" alt="Edit"></button>
|
||||
</td>
|
||||
<td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td>
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly="" class="inline selectall"></td>
|
||||
<td>
|
||||
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
|
||||
class="small_icon" alt="Delete"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="button" class="blue inline" data-name="sharebytoken"><img src="css/icons/new.svg"
|
||||
class="small_icon" alt="New Share by Token"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
<div data-name="sharebymap">
|
||||
<h2>By Map</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr data-name="sharemaprowtemplate" class="hidden">
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="edit"><img src="css/icons/edit.svg"
|
||||
class="small_icon" alt="Edit"></button>
|
||||
</td>
|
||||
<td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td>
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly=""
|
||||
onfocus="this.setSelectionRange(0, 99999);" class="inline"></td>
|
||||
<td>
|
||||
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
|
||||
class="small_icon" alt="Delete"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="sharebymap">
|
||||
<img src="css/icons/new.svg" class="small_icon" alt="New Share by Map">
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<details open>
|
||||
<summary>By Map</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr data-name="sharemaprowtemplate" class="hidden">
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="edit"><img src="css/icons/edit.svg"
|
||||
class="small_icon" alt="Edit"></button>
|
||||
</td>
|
||||
<td><span class="pill" data-name="rw">rw</span><span class="pill" data-name="ro">ro</span></td>
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly="" class="inline selectall"></td>
|
||||
<td>
|
||||
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
|
||||
class="small_icon" alt="Delete"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="sharebymap">
|
||||
<img src="css/icons/new.svg" class="small_icon" alt="New Share by Map">
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
<div data-name="sharebybday">
|
||||
<details open>
|
||||
<summary>shared as bday calendar</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr data-name="sharebdayrowtemplate" class="hidden">
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="edit"><img src="css/icons/edit.svg"
|
||||
class="small_icon" alt="Edit"></button>
|
||||
</td>
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly="" class="inline selectall"></td>
|
||||
<td>
|
||||
<button type="button" class="red inline" data-name="delete"><img src="css/icons/delete.svg"
|
||||
class="small_icon" alt="Delete"></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button type="button" class="blue inline" data-name="sharebybday">
|
||||
<img src="css/icons/new.svg" class="small_icon" alt="New Share as Bday Calendar">
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
<form>
|
||||
<button type="button" class="green" data-name="cancel">Close</button>
|
||||
@@ -224,8 +249,7 @@
|
||||
</thead>
|
||||
<tbody data-name="incomingsharesbody">
|
||||
<tr data-name="incomingsharerowtemplate" class="hidden">
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly=""
|
||||
onfocus="this.setSelectionRange(0, 99999);" class="inline"></td>
|
||||
<td><input type="text" data-name="pathortoken" value="" readonly="" class="inline selectall"></td>
|
||||
<td data-name="owner"></td>
|
||||
<td data-name="permissions"><span class="pill" data-name="rw">rw</span><span class="pill"
|
||||
data-name="ro">ro</span></td>
|
||||
@@ -244,29 +268,34 @@
|
||||
<section id="newshare" class="container hidden">
|
||||
<h1>New Share</h1>
|
||||
<form>
|
||||
<fieldset data-name="sharemapfields" class="hidden">
|
||||
<legend>Map Target</legend>
|
||||
<details open data-name="sharemapfields" class="hidden">
|
||||
<summary>Map Target</summary>
|
||||
<label for="newshare_attr_shareuser">Share User</label>
|
||||
<input type="text" data-name="shareuser" id="newshare_attr_shareuser" />
|
||||
<label for="newshare_attr_sharehref">Share Href</label>
|
||||
<input type="text" data-name="sharehref" id="newshare_attr_sharehref" />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Attributes</legend>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>Attributes</summary>
|
||||
<input type="checkbox" data-name="enabled" checked id="newshare_attr_enabled" /><label
|
||||
for="newshare_attr_enabled">Enabled</label>
|
||||
<input type="checkbox" data-name="hidden" id="newshare_attr_hidden" /><label
|
||||
for="newshare_attr_hidden">Hidden</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Permissions</legend>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>Permissions</summary>
|
||||
<input type="radio" data-name="permissions" checked="true" id="newshare_attr_permissions_ro"
|
||||
name="newshare_permissions"><label for="newshare_attr_permissions_ro">Readonly</label>
|
||||
<input type="radio" data-name="permissions" checked="false" id="newshare_attr_permissions_rw"
|
||||
name="newshare_permissions" /><label for="newshare_attr_permissions_rw">Read/Write</label>
|
||||
</fieldset>
|
||||
<fieldset data-name="properties_override">
|
||||
<legend>Properties override</legend>
|
||||
</details>
|
||||
<details data-name="properties_override">
|
||||
<summary>Properties override</summary>
|
||||
<div class="property-override">
|
||||
<input type="checkbox" data-name="displayname_override_enabled" id="newshare_attr_displayname_enabled">
|
||||
<label for="newshare_attr_displayname_enabled">Displayname:</label>
|
||||
<input type="text" data-name="displayname_override" disabled>
|
||||
</div>
|
||||
<div class="property-override">
|
||||
<input type="checkbox" data-name="description_override_enabled" id="newshare_attr_description_enabled">
|
||||
<label for="newshare_attr_description_enabled">Description:</label>
|
||||
@@ -277,7 +306,7 @@
|
||||
<label for="newshare_attr_color_enabled">Color:</label>
|
||||
<input type="color" data-name="color_override" disabled>
|
||||
</div>
|
||||
</fieldset>
|
||||
</details>
|
||||
<button type="submit" class="green" data-name="submit">Create</button>
|
||||
<button type="button" class="red" data-name="cancel">Cancel</button>
|
||||
</form>
|
||||
@@ -344,10 +373,10 @@
|
||||
<br>
|
||||
</section>
|
||||
|
||||
<section id="deletecollectionscene" class="container hidden">
|
||||
<h1>Delete Collection</h1>
|
||||
<p>To delete the collection <span class="title" data-name="title">title</span> please enter the phrase <strong
|
||||
data-name="deleteconfirmationtext"></strong> in the box below:</p>
|
||||
<section id="deleteconfirmationscene" class="container hidden">
|
||||
<h1 data-name="headertitle">Delete Collection</h1>
|
||||
<p data-name="confirmationprompt">To delete the collection <span class="title" data-name="title">title</span>
|
||||
please enter the phrase <strong data-name="deleteconfirmationtext"></strong> in the box below:</p>
|
||||
<input type="text" class="deleteconfirmationtxt" data-name="confirmationtxt" />
|
||||
<p class="red">WARNING: This action cannot be reversed.</p>
|
||||
<form>
|
||||
|
||||
@@ -27,6 +27,8 @@ import { CollectionType } from "../models/collection.js";
|
||||
* @property {boolean} [PermittedCreateCollectionByMap]
|
||||
* @property {boolean} [FeatureEnabledCollectionByToken]
|
||||
* @property {boolean} [PermittedCreateCollectionByToken]
|
||||
* @property {boolean} [FeatureEnabledCollectionByBday]
|
||||
* @property {boolean} [PermittedCreateCollectionByBday]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -182,10 +184,13 @@ export const OVERLAY_PROPERTIES = {
|
||||
/**
|
||||
* Returns the correct internal property key for a given collection type and property name.
|
||||
* @param {string} type Collection type (ADDRESSBOOK, CALENDAR, etc.)
|
||||
* @param {"DESCRIPTION" | "COLOR"} property Property name
|
||||
* @param {"DISPLAYNAME" | "DESCRIPTION" | "COLOR"} property Property name
|
||||
* @returns {string | null} Internal property key or null if not supported
|
||||
*/
|
||||
export function get_property_key(type, property) {
|
||||
if (property === "DISPLAYNAME") {
|
||||
return "D:displayname";
|
||||
}
|
||||
if (type === CollectionType.ADDRESSBOOK) {
|
||||
return OVERLAY_PROPERTIES.ADDRESSBOOK[property];
|
||||
} else if (CollectionType.is_subset(CollectionType.CALENDAR, type)) {
|
||||
@@ -430,7 +435,7 @@ export function update_incoming_share(
|
||||
call_sharing_api(
|
||||
user,
|
||||
password,
|
||||
"map/update",
|
||||
share.ShareType + "/update",
|
||||
{
|
||||
PathOrToken: share.PathOrToken,
|
||||
Enabled: share.EnabledByUser,
|
||||
@@ -450,3 +455,115 @@ export function update_incoming_share(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
export function add_share_by_bday(
|
||||
user,
|
||||
password,
|
||||
share,
|
||||
callback,
|
||||
) {
|
||||
call_sharing_api(
|
||||
user,
|
||||
password,
|
||||
"bday/create",
|
||||
{
|
||||
PathMapped: share.PathMapped,
|
||||
Permissions: share.Permissions,
|
||||
Enabled: share.EnabledByOwner,
|
||||
Hidden: share.HiddenByOwner,
|
||||
Properties: share.Properties,
|
||||
User: share.User,
|
||||
PathOrToken: share.PathOrToken,
|
||||
},
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
if (json_response["Status"] !== "success") {
|
||||
callback(json_response["Status"] || "Unknown error");
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
null,
|
||||
function (error) {
|
||||
callback(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
export function delete_share_by_bday(
|
||||
user,
|
||||
password,
|
||||
share,
|
||||
callback,
|
||||
) {
|
||||
call_sharing_api(
|
||||
user,
|
||||
password,
|
||||
"bday/delete",
|
||||
{ PathOrToken: share.PathOrToken },
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
if (json_response["Status"] !== "success") {
|
||||
callback(json_response["Status"] || "Unknown error");
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
null,
|
||||
function (error) {
|
||||
callback(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
export function update_share_by_bday(
|
||||
user,
|
||||
password,
|
||||
share,
|
||||
callback,
|
||||
) {
|
||||
call_sharing_api(
|
||||
user,
|
||||
password,
|
||||
"bday/update",
|
||||
{
|
||||
PathOrToken: share.PathOrToken,
|
||||
PathMapped: share.PathMapped,
|
||||
User: share.User,
|
||||
Permissions: share.Permissions,
|
||||
Enabled: share.EnabledByOwner,
|
||||
Hidden: share.HiddenByOwner,
|
||||
Properties: share.Properties,
|
||||
},
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
if (json_response["Status"] !== "success") {
|
||||
callback(json_response["Status"] || "Unknown error");
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
null,
|
||||
function (error) {
|
||||
callback(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,17 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LoginScene } from "./scenes/LoginScene.js";
|
||||
import { LoadingScene } from "./scenes/LoadingScene.js";
|
||||
import { LoginScene } from "./scenes/LoginScene.js";
|
||||
import { push_scene } from "./scenes/scene_manager.js";
|
||||
import { setupSelectAll } from "./utils/misc.js";
|
||||
|
||||
// Add selection handler for input fields with 'selectall' class.
|
||||
setupSelectAll();
|
||||
|
||||
// Hide startup loading message
|
||||
// This works because the LoadingScene is the one that is already active in index.html,
|
||||
// and all other scenes are hidden.
|
||||
new LoadingScene().hide();
|
||||
push_scene(new LoginScene());
|
||||
push_scene(new LoginScene());
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { delete_collection } from "../api/api.js";
|
||||
import { SERVER } from "../constants.js";
|
||||
import { Collection, CollectionType } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { bytesToHumanReadable } from "../utils/misc.js";
|
||||
import { CreateEditCollectionScene } from "./CreateEditCollectionScene.js";
|
||||
import { DeleteCollectionScene } from "./DeleteCollectionScene.js";
|
||||
import { DeleteConfirmationScene } from "./DeleteConfirmationScene.js";
|
||||
import { IncomingSharingScene } from "./IncomingSharingScene.js";
|
||||
import { Scene, push_scene } from "./scene_manager.js";
|
||||
import { ShareCollectionScene, maybe_enable_sharing_options } from "./ShareCollectionScene.js";
|
||||
@@ -111,7 +112,10 @@ export class CollectionsScene {
|
||||
*/
|
||||
function ondelete(collection) {
|
||||
try {
|
||||
let delete_collection_scene = new DeleteCollectionScene(user, password, collection);
|
||||
let delete_collection_scene = new DeleteConfirmationScene(
|
||||
user, password, "Delete Collection", collection, collection.displayname || collection.href,
|
||||
delete_collection, true
|
||||
);
|
||||
push_scene(delete_collection_scene);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -166,10 +170,17 @@ export class CollectionsScene {
|
||||
}
|
||||
});
|
||||
let share_info = node.querySelector("[data-name=shared-by]");
|
||||
let share = (shares || []).find(s => s.ShareType === "map" && (s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
|
||||
if (share && share.Owner !== user) {
|
||||
share_info.classList.remove("hidden");
|
||||
node.querySelector("[data-name=shared-by-owner]").textContent = share.Owner;
|
||||
let transformed_from = node.querySelector("[data-name=transformed-from]");
|
||||
let share = (shares || []).find(
|
||||
s => (s.ShareType === "map" || s.ShareType === "bday") &&
|
||||
(s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
|
||||
if (share) {
|
||||
if (share.Owner !== user) {
|
||||
share_info.classList.remove("hidden");
|
||||
node.querySelector("[data-name=shared-by-owner]").textContent = share.Owner;
|
||||
} else {
|
||||
transformed_from.classList.remove("hidden");
|
||||
}
|
||||
let share_option = node.querySelector("[data-name=shareoption]");
|
||||
if (share_option) {
|
||||
share_option.classList.add("hidden");
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Share, add_share_by_map, add_share_by_token, get_property_key, update_share_by_map, update_share_by_token } from "../api/sharing.js";
|
||||
import { Share, add_share_by_bday, add_share_by_map, add_share_by_token, get_property_key, update_share_by_bday, update_share_by_map, update_share_by_token } from "../api/sharing.js";
|
||||
import { CollectionType } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_href, validate_not_empty_or_equals } from "../utils/form_validator.js";
|
||||
import { FormValidator, validate_href, validate_non_empty, validate_not_empty_or_equals } from "../utils/form_validator.js";
|
||||
import { onCleanHREFinput, random_uuid } from "../utils/misc.js";
|
||||
import { Scene, is_current_scene, pop_scene } from "./scene_manager.js";
|
||||
|
||||
@@ -51,7 +52,9 @@ export class CreateEditShareScene {
|
||||
let permissions_ro_radio = /** @type {HTMLInputElement} */ (document.getElementById("newshare_attr_permissions_ro"));
|
||||
let permissions_rw_radio = /** @type {HTMLInputElement} */ (document.getElementById("newshare_attr_permissions_rw"));
|
||||
|
||||
/** @type {HTMLElement} */ let properties_fieldset = html_scene.querySelector("[data-name=properties_override]");
|
||||
/** @type {HTMLDetailsElement} */ let properties_fieldset = /** @type {HTMLDetailsElement} */ (html_scene.querySelector("[data-name=properties_override]"));
|
||||
/** @type {HTMLInputElement} */ let displayname_override_enabled = html_scene.querySelector("[data-name=displayname_override_enabled]");
|
||||
/** @type {HTMLInputElement} */ let displayname_override_input = html_scene.querySelector("[data-name=displayname_override]");
|
||||
/** @type {HTMLInputElement} */ let description_override_enabled = html_scene.querySelector("[data-name=description_override_enabled]");
|
||||
/** @type {HTMLInputElement} */ let description_override_input = html_scene.querySelector("[data-name=description_override]");
|
||||
/** @type {HTMLInputElement} */ let color_override_enabled = html_scene.querySelector("[data-name=color_override_enabled]");
|
||||
@@ -64,11 +67,19 @@ export class CreateEditShareScene {
|
||||
let errorHandler = new ErrorHandler(error_form);
|
||||
let map_validator = new FormValidator(errorHandler);
|
||||
|
||||
map_validator.addValidator(shareuser_input, validate_not_empty_or_equals(shareuser_input, user, "Share User"));
|
||||
if (shareType === "bday") {
|
||||
// bday can share to self, so just validate non-empty
|
||||
map_validator.addValidator(shareuser_input, validate_non_empty(shareuser_input, "Share User"));
|
||||
} else {
|
||||
map_validator.addValidator(shareuser_input, validate_not_empty_or_equals(shareuser_input, user, "Share User"));
|
||||
}
|
||||
map_validator.addValidator(sharehref_input, validate_href(sharehref_input, "Share Href"));
|
||||
|
||||
sharehref_input.addEventListener("input", onCleanHREFinput);
|
||||
|
||||
displayname_override_enabled.onchange = function () {
|
||||
displayname_override_input.disabled = !displayname_override_enabled.checked;
|
||||
};
|
||||
description_override_enabled.onchange = function () {
|
||||
description_override_input.disabled = !description_override_enabled.checked;
|
||||
};
|
||||
@@ -88,16 +99,20 @@ export class CreateEditShareScene {
|
||||
|
||||
function onsubmit() {
|
||||
try {
|
||||
if (shareType === "map") {
|
||||
if (shareType === "map" || shareType === "bday") {
|
||||
if (!map_validator.validate()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let enabled_by_owner = enabled_checkbox.checked;
|
||||
let hidden_by_owner = hidden_checkbox.checked;
|
||||
let enabled_by_owner = (shareType === "bday" && shareuser_input.value === user) ? true : enabled_checkbox.checked;
|
||||
let hidden_by_owner = (shareType === "bday" && shareuser_input.value === user) ? false : hidden_checkbox.checked;
|
||||
let permissions = permissions_rw_radio.checked ? "rw" : "r";
|
||||
|
||||
let properties = {};
|
||||
if (displayname_override_enabled.checked) {
|
||||
let key = get_property_key(collection.type, "DISPLAYNAME");
|
||||
if (key) properties[key] = displayname_override_input.value;
|
||||
}
|
||||
if (description_override_enabled.checked) {
|
||||
let key = get_property_key(collection.type, "DESCRIPTION");
|
||||
if (key) properties[key] = description_override_input.value;
|
||||
@@ -114,6 +129,11 @@ export class CreateEditShareScene {
|
||||
if (error) {
|
||||
errorHandler.setError(error);
|
||||
} else {
|
||||
// For bday shares to the current user, invalidate the
|
||||
// collections cache so the virtual calendar appears immediately.
|
||||
if (shareType === "bday" && new_share.User === user) {
|
||||
collectionsCache.invalidate();
|
||||
}
|
||||
pop_scene();
|
||||
}
|
||||
};
|
||||
@@ -128,18 +148,22 @@ export class CreateEditShareScene {
|
||||
HiddenByUser: edit ? share.HiddenByUser : null,
|
||||
Properties: properties,
|
||||
User: edit ? share.User : shareuser_input.value,
|
||||
PathOrToken: edit ? share.PathOrToken : (shareType === "map" ? "/" + shareuser_input.value + "/" + sharehref_input.value + "/" : ""),
|
||||
PathOrToken: edit ? share.PathOrToken : ((shareType === "map" || shareType === "bday") ? "/" + shareuser_input.value + "/" + sharehref_input.value + "/" : ""),
|
||||
});
|
||||
|
||||
if (edit) {
|
||||
if (shareType === "map") {
|
||||
update_share_by_map(user, password, new_share, callback);
|
||||
} else if (shareType === "bday") {
|
||||
update_share_by_bday(user, password, new_share, callback);
|
||||
} else {
|
||||
update_share_by_token(user, password, new_share, callback);
|
||||
}
|
||||
} else {
|
||||
if (shareType === "map") {
|
||||
add_share_by_map(user, password, new_share, callback);
|
||||
} else if (shareType === "bday") {
|
||||
add_share_by_bday(user, password, new_share, callback);
|
||||
} else {
|
||||
add_share_by_token(user, password, new_share, callback);
|
||||
}
|
||||
@@ -153,32 +177,59 @@ export class CreateEditShareScene {
|
||||
this.show = function () {
|
||||
this.release();
|
||||
html_scene.classList.remove("hidden");
|
||||
html_scene.querySelectorAll("details").forEach(function (details) {
|
||||
if (details.dataset.name !== "properties_override") {
|
||||
details.open = true;
|
||||
} else {
|
||||
details.open = false;
|
||||
}
|
||||
});
|
||||
cancel_btn.onclick = oncancel;
|
||||
form.onsubmit = onsubmit;
|
||||
|
||||
html_scene.querySelector("h1").textContent = edit ? "Edit Share" : "New Share";
|
||||
submit_btn.textContent = edit ? "Save" : "Create";
|
||||
|
||||
shareuser_input.value = edit ? share.User : "";
|
||||
shareuser_input.value = edit ? share.User : (shareType === "bday" ? user : "");
|
||||
shareuser_input.disabled = edit;
|
||||
enabled_checkbox.checked = edit ? share.EnabledByOwner : true;
|
||||
hidden_checkbox.checked = edit ? share.HiddenByOwner : false;
|
||||
permissions_ro_radio.checked = edit ? share.Permissions.toLowerCase() === "r" : true;
|
||||
permissions_rw_radio.checked = edit ? share.Permissions.toLowerCase() === "rw" : false;
|
||||
|
||||
if (shareType === "bday") {
|
||||
// bday is always read-only; hide the permissions section entirely
|
||||
permissions_ro_radio.checked = true;
|
||||
permissions_rw_radio.checked = false;
|
||||
permissions_ro_radio.disabled = true;
|
||||
permissions_rw_radio.disabled = true;
|
||||
permissions_ro_radio.closest("details")?.classList.add("hidden");
|
||||
} else {
|
||||
permissions_ro_radio.closest("details")?.classList.remove("hidden");
|
||||
permissions_ro_radio.disabled = false;
|
||||
permissions_rw_radio.disabled = false;
|
||||
permissions_ro_radio.checked = edit ? share.Permissions.toLowerCase() === "r" : true;
|
||||
permissions_rw_radio.checked = edit ? share.Permissions.toLowerCase() === "rw" : false;
|
||||
}
|
||||
|
||||
let displayname = collection.displayname || "";
|
||||
let description = collection.description || "";
|
||||
let color = collection.color || "#ffffff";
|
||||
let displayname_override_enabled_value = false;
|
||||
let description_override_enabled_value = false;
|
||||
let color_override_enabled_value = false;
|
||||
|
||||
if (edit && share.Properties) {
|
||||
let displayname_key = get_property_key(collection.type, "DISPLAYNAME");
|
||||
if (displayname_key && share.Properties[displayname_key] !== undefined) {
|
||||
displayname = share.Properties[displayname_key];
|
||||
displayname_override_enabled_value = true;
|
||||
}
|
||||
let description_key = get_property_key(collection.type, "DESCRIPTION");
|
||||
if (description_key && share.Properties[description_key]) {
|
||||
if (description_key && share.Properties[description_key] !== undefined) {
|
||||
description = share.Properties[description_key];
|
||||
description_override_enabled_value = true;
|
||||
}
|
||||
let color_key = get_property_key(collection.type, "COLOR");
|
||||
if (color_key && share.Properties[color_key]) {
|
||||
if (color_key && share.Properties[color_key] !== undefined) {
|
||||
color = share.Properties[color_key];
|
||||
if (color.length === 9 && color.endsWith("ff")) {
|
||||
color = color.substring(0, 7);
|
||||
@@ -187,6 +238,10 @@ export class CreateEditShareScene {
|
||||
}
|
||||
}
|
||||
|
||||
displayname_override_enabled.checked = displayname_override_enabled_value;
|
||||
displayname_override_input.value = displayname;
|
||||
displayname_override_input.disabled = !displayname_override_enabled_value;
|
||||
|
||||
description_override_enabled.checked = description_override_enabled_value;
|
||||
description_override_input.value = description;
|
||||
description_override_input.disabled = !description_override_enabled_value;
|
||||
@@ -197,13 +252,19 @@ export class CreateEditShareScene {
|
||||
|
||||
let is_calendar = CollectionType.is_subset(CollectionType.CALENDAR, collection.type);
|
||||
let is_addressbook = collection.type === CollectionType.ADDRESSBOOK;
|
||||
properties_fieldset.classList.remove("hidden");
|
||||
if (displayname_override_enabled_value || description_override_enabled_value || color_override_enabled_value) {
|
||||
properties_fieldset.open = true;
|
||||
}
|
||||
if (is_calendar || is_addressbook) {
|
||||
properties_fieldset.classList.remove("hidden");
|
||||
description_override_enabled.parentElement.classList.remove("hidden");
|
||||
color_override_enabled.parentElement.classList.remove("hidden");
|
||||
} else {
|
||||
properties_fieldset.classList.add("hidden");
|
||||
description_override_enabled.parentElement.classList.add("hidden");
|
||||
color_override_enabled.parentElement.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (shareType === "map") {
|
||||
if (shareType === "map" || shareType === "bday") {
|
||||
if (edit) {
|
||||
sharehref_input.value = share.PathOrToken.split("/").filter(Boolean).pop() || "";
|
||||
} else {
|
||||
|
||||
@@ -19,62 +19,83 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { delete_collection } from "../api/api.js";
|
||||
import { DELETE_CONFIRMATION_TEXT } from "../constants.js";
|
||||
import { Collection } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_equals } from "../utils/form_validator.js";
|
||||
import { LoadingScene } from "./LoadingScene.js";
|
||||
import { Scene, is_current_scene, pop_scene, pop_to_parent, push_scene } from "./scene_manager.js";
|
||||
import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.js";
|
||||
|
||||
/**
|
||||
* @implements {Scene}
|
||||
*/
|
||||
export class DeleteCollectionScene {
|
||||
export class DeleteConfirmationScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {Collection} collection
|
||||
* @param {string} header_title
|
||||
* @param {any} item
|
||||
* @param {string} item_title
|
||||
* @param {function} delete_action
|
||||
* @param {boolean} needsconfirmation
|
||||
* @param {function} [on_success]
|
||||
*/
|
||||
constructor(user, password, collection) {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("deletecollectionscene");
|
||||
constructor(user, password, header_title, item, item_title, delete_action, needsconfirmation, on_success) {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("deleteconfirmationscene");
|
||||
/** @type {HTMLElement} */ let header_html = html_scene.querySelector("[data-name=headertitle]");
|
||||
if (header_html) header_html.textContent = header_title;
|
||||
/** @type {HTMLElement} */ let title_form = html_scene.querySelector("[data-name=title]");
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let confirmation_prompt = html_scene.querySelector("[data-name=confirmationprompt]");
|
||||
/** @type {HTMLInputElement} */ let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]");
|
||||
/** @type {HTMLElement} */ let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]");
|
||||
/** @type {HTMLElement} */ let delete_btn = html_scene.querySelector("[data-name=delete]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
||||
|
||||
delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
|
||||
confirmation_txt.value = "";
|
||||
confirmation_txt.addEventListener("keydown", onkeydown);
|
||||
if (needsconfirmation) {
|
||||
delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
|
||||
confirmation_txt.value = "";
|
||||
confirmation_txt.addEventListener("keydown", onkeydown);
|
||||
confirmation_prompt.classList.remove("hidden");
|
||||
confirmation_txt.classList.remove("hidden");
|
||||
} else {
|
||||
confirmation_prompt.classList.add("hidden");
|
||||
confirmation_txt.classList.add("hidden");
|
||||
confirmation_txt.removeEventListener("keydown", onkeydown);
|
||||
}
|
||||
|
||||
/** @type {?XMLHttpRequest} */ let delete_req = null;
|
||||
|
||||
let errorHandler = new ErrorHandler(error_form);
|
||||
let validator = new FormValidator(errorHandler);
|
||||
|
||||
validator.addValidator(confirmation_txt, validate_equals(confirmation_txt, DELETE_CONFIRMATION_TEXT, "confirmation"));
|
||||
if (needsconfirmation) {
|
||||
validator.addValidator(confirmation_txt, validate_equals(confirmation_txt, DELETE_CONFIRMATION_TEXT, "confirmation"));
|
||||
}
|
||||
|
||||
function ondelete() {
|
||||
if (!validator.validate()) {
|
||||
if (needsconfirmation && !validator.validate()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
let loading_scene = new LoadingScene();
|
||||
push_scene(loading_scene);
|
||||
delete_req = delete_collection(user, password, collection, function (error1) {
|
||||
delete_req = delete_action(user, password, item, function (error1) {
|
||||
if (!is_current_scene(loading_scene)) {
|
||||
return;
|
||||
}
|
||||
delete_req = null;
|
||||
if (error1) {
|
||||
errorHandler.setError(error1);
|
||||
pop_scene();
|
||||
errorHandler.setError(error1);
|
||||
} else {
|
||||
collectionsCache.invalidate();
|
||||
pop_to_parent();
|
||||
pop_scene();
|
||||
if (on_success) {
|
||||
on_success();
|
||||
} else {
|
||||
collectionsCache.invalidate();
|
||||
pop_scene();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -102,7 +123,7 @@ export class DeleteCollectionScene {
|
||||
this.show = function () {
|
||||
this.release();
|
||||
html_scene.classList.remove("hidden");
|
||||
title_form.textContent = collection.displayname || collection.href;
|
||||
title_form.textContent = item_title;
|
||||
delete_btn.onclick = ondelete;
|
||||
cancel_btn.onclick = oncancel;
|
||||
validator.validate();
|
||||
@@ -95,7 +95,9 @@ export class IncomingSharingScene {
|
||||
nodes = [];
|
||||
|
||||
let prefix = "/" + user + "/";
|
||||
let filtered_shares = shares.filter(share => share.ShareType === "map" && share.PathOrToken.startsWith(prefix));
|
||||
let filtered_shares = shares.filter(
|
||||
share => (share.ShareType === "map" || share.ShareType === "bday")
|
||||
&& share.PathOrToken.startsWith(prefix));
|
||||
|
||||
if (filtered_shares.length === 0) {
|
||||
table.classList.add("hidden");
|
||||
|
||||
@@ -20,15 +20,17 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
delete_share_by_bday,
|
||||
delete_share_by_map,
|
||||
delete_share_by_token,
|
||||
reload_sharing_list,
|
||||
} from "../api/sharing.js";
|
||||
import { Collection } from "../models/collection.js";
|
||||
import { Collection, CollectionType } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { displayPermissions } from "../utils/permissions.js";
|
||||
import { CreateEditShareScene } from "./CreateEditShareScene.js";
|
||||
import { DeleteConfirmationScene } from "./DeleteConfirmationScene.js";
|
||||
import { Scene, pop_scene, push_scene } from "./scene_manager.js";
|
||||
|
||||
/**
|
||||
@@ -50,6 +52,9 @@ export class ShareCollectionScene {
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_map_btn = html_scene.querySelector(
|
||||
"button[data-name=sharebymap]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_bday_btn = html_scene.querySelector(
|
||||
"button[data-name=sharebybday]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_token_div = html_scene.querySelector(
|
||||
"div[data-name=sharebytoken]"
|
||||
@@ -57,6 +62,9 @@ export class ShareCollectionScene {
|
||||
/** @type {HTMLElement} */ let share_by_map_div = html_scene.querySelector(
|
||||
"div[data-name=sharebymap]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_bday_div = html_scene.querySelector(
|
||||
"div[data-name=sharebybday]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
|
||||
let errorHandler = new ErrorHandler(error_form);
|
||||
@@ -82,9 +90,17 @@ export class ShareCollectionScene {
|
||||
push_scene(create_edit_share_scene);
|
||||
}
|
||||
|
||||
function onsharebybday() {
|
||||
let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "bday");
|
||||
push_scene(create_edit_share_scene);
|
||||
}
|
||||
|
||||
this.show = function () {
|
||||
this.release();
|
||||
html_scene.classList.remove("hidden");
|
||||
html_scene.querySelectorAll("details").forEach(function (details) {
|
||||
details.open = true;
|
||||
});
|
||||
cancel_btn.onclick = oncancel;
|
||||
|
||||
collectionsCache.getServerFeatures(user, password, errorHandler.setError, (features) => {
|
||||
@@ -117,6 +133,26 @@ export class ShareCollectionScene {
|
||||
} else {
|
||||
if (share_by_map_div) share_by_map_div.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (features.sharing && features.sharing.PermittedCreateCollectionByBday) {
|
||||
let is_addressbook = collection.type === CollectionType.ADDRESSBOOK;
|
||||
if (share_by_bday_btn) {
|
||||
if (is_addressbook) {
|
||||
share_by_bday_btn.classList.remove("hidden");
|
||||
share_by_bday_btn.onclick = onsharebybday;
|
||||
} else {
|
||||
share_by_bday_btn.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (share_by_bday_btn) share_by_bday_btn.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (features.sharing && features.sharing.FeatureEnabledCollectionByBday && collection.type === CollectionType.ADDRESSBOOK) {
|
||||
if (share_by_bday_div) share_by_bday_div.classList.remove("hidden");
|
||||
} else {
|
||||
if (share_by_bday_div) share_by_bday_div.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
title.textContent = collection.displayname || collection.href;
|
||||
@@ -139,7 +175,7 @@ export class ShareCollectionScene {
|
||||
*/
|
||||
function update_share_list(user, password, collection, errorHandler) {
|
||||
let share_rows = document.querySelectorAll(
|
||||
"[data-name=sharetokenrowtemplate], [data-name=sharemaprowtemplate]",
|
||||
"[data-name=sharetokenrowtemplate], [data-name=sharemaprowtemplate], [data-name=sharebdayrowtemplate]",
|
||||
);
|
||||
share_rows.forEach(function (row) {
|
||||
if (!row.classList.contains("hidden")) {
|
||||
@@ -166,8 +202,9 @@ function update_share_list(user, password, collection, errorHandler) {
|
||||
* @param {string} delete_label
|
||||
* @param {function(string, string, import('../api/sharing.js').Share, function(?string):void):void} delete_action
|
||||
* @param {ErrorHandler} errorHandler
|
||||
* @param {function():void} [onDeleteSuccess] Optional extra callback after a successful delete.
|
||||
*/
|
||||
function add_share_row_node(user, password, collection, share, template, delete_label, delete_action, errorHandler) {
|
||||
function add_share_row_node(user, password, collection, share, template, delete_label, delete_action, errorHandler, onDeleteSuccess) {
|
||||
let pathortoken = share["PathOrToken"] || "";
|
||||
let node = /** @type {HTMLElement} */ (template.cloneNode(true));
|
||||
node.classList.remove("hidden");
|
||||
@@ -188,21 +225,15 @@ function add_share_row_node(user, password, collection, share, template, delete_
|
||||
|
||||
/** @type {HTMLElement} */ let delete_btn = node.querySelector("[data-name=delete]");
|
||||
delete_btn.onclick = function () {
|
||||
if (!confirm("Are you sure you want to delete " + delete_label + " " + pathortoken + "?")) {
|
||||
return;
|
||||
}
|
||||
delete_action(
|
||||
user,
|
||||
password,
|
||||
share,
|
||||
function (error) {
|
||||
if (error) {
|
||||
errorHandler.setError(error);
|
||||
} else {
|
||||
update_share_list(user, password, collection, errorHandler);
|
||||
}
|
||||
},
|
||||
let delete_collection_scene = new DeleteConfirmationScene(
|
||||
user, password, "Delete Share", share, delete_label + " " + pathortoken, delete_action, false,
|
||||
function () {
|
||||
if (onDeleteSuccess) onDeleteSuccess();
|
||||
pop_scene();
|
||||
update_share_list(user, password, collection, errorHandler);
|
||||
}
|
||||
);
|
||||
push_scene(delete_collection_scene);
|
||||
};
|
||||
|
||||
template.parentNode.insertBefore(node, template);
|
||||
@@ -218,6 +249,7 @@ function add_share_row_node(user, password, collection, share, template, delete_
|
||||
function add_share_rows(user, password, collection, shares, errorHandler) {
|
||||
/** @type {HTMLElement} */ let token_template = document.querySelector("[data-name=sharetokenrowtemplate]");
|
||||
/** @type {HTMLElement} */ let map_template = document.querySelector("[data-name=sharemaprowtemplate]");
|
||||
/** @type {HTMLElement} */ let bday_template = document.querySelector("[data-name=sharebdayrowtemplate]");
|
||||
shares.forEach(function (share) {
|
||||
let pathortoken = share["PathOrToken"] || "";
|
||||
let pathmapped = share["PathMapped"] || "";
|
||||
@@ -229,6 +261,9 @@ function add_share_rows(user, password, collection, shares, errorHandler) {
|
||||
add_share_row_node(user, password, collection, share, token_template, "share", delete_share_by_token, errorHandler);
|
||||
} else if (share["ShareType"] === "map") {
|
||||
add_share_row_node(user, password, collection, share, map_template, "map", delete_share_by_map, errorHandler);
|
||||
} else if (share["ShareType"] === "bday") {
|
||||
let onDeleteSuccess = (share["User"] === user) ? function () { collectionsCache.invalidate(); } : undefined;
|
||||
add_share_row_node(user, password, collection, share, bday_template, "bday", delete_share_by_bday, errorHandler, onDeleteSuccess);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -241,7 +276,8 @@ export function maybe_enable_sharing_options(features) {
|
||||
if (!features || !features.sharing) return;
|
||||
let map_is_enabled = features.sharing.FeatureEnabledCollectionByMap || false;
|
||||
let token_is_enabled = features.sharing.FeatureEnabledCollectionByToken || false;
|
||||
if (map_is_enabled || token_is_enabled) {
|
||||
let bday_is_enabled = features.sharing.FeatureEnabledCollectionByBday || false;
|
||||
if (map_is_enabled || token_is_enabled || bday_is_enabled) {
|
||||
let share_options = document.querySelectorAll("[data-name=shareoption]");
|
||||
for (let i = 0; i < share_options.length; i++) {
|
||||
let share_option = share_options[i];
|
||||
|
||||
@@ -108,3 +108,14 @@ export function bytesToHumanReadable(bytes) {
|
||||
i = Math.min(i, units.length - 1);
|
||||
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + units[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add selection handler for input fields with 'selectall' class.
|
||||
*/
|
||||
export function setupSelectAll() {
|
||||
document.addEventListener("focusin", (event) => {
|
||||
if (event.target instanceof HTMLInputElement && event.target.classList.contains("selectall")) {
|
||||
event.target.setSelectionRange(0, 99999);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user