adds public lists

This commit is contained in:
Christian Beutel
2024-12-08 21:30:55 +01:00
parent 254e053bd9
commit 673eb78dd0
15 changed files with 200 additions and 59 deletions

View File

@@ -0,0 +1,60 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/types"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69")
if err != nil {
return err
}
collection.ListRule = types.Pointer("author = @request.auth.id || public = true || (@request.auth.id != \"\" && list_share_via_list.user ?= @request.auth.id)")
collection.ViewRule = types.Pointer("author = @request.auth.id || public = true || (@request.auth.id != \"\" && list_share_via_list.user ?= @request.auth.id)")
// add
new_public := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "rolk3q3j",
"name": "public",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
}`), new_public); err != nil {
return err
}
collection.Schema.AddField(new_public)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69")
if err != nil {
return err
}
collection.ListRule = types.Pointer("author = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.user ?= @request.auth.id)")
collection.ViewRule = types.Pointer("author = @request.auth.id || (@request.auth.id != \"\" && list_share_via_list.user ?= @request.auth.id)")
// remove
collection.Schema.RemoveField("rolk3q3j")
return dao.SaveCollection(collection)
})
}

View File

@@ -9,6 +9,7 @@
export let title: string = $_('confirm-deletion'); export let title: string = $_('confirm-deletion');
export let text: string; export let text: string;
export let action: string = "delete"; export let action: string = "delete";
export let id: string = "confirm-modal";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -18,13 +19,13 @@
} }
</script> </script>
<Modal id="confirm-modal" {title} bind:openModal bind:closeModal> <Modal {id} {title} bind:openModal bind:closeModal>
<p slot="content">{text}</p> <p slot="content">{text}</p>
<div slot="footer" class="flex items-center gap-4"> <div slot="footer" class="flex items-center gap-4">
<button class="btn-secondary" on:click={closeModal} <button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button >{$_("cancel")}</button
> >
<button class="btn-danger" type="button" on:click={confirm} name="delete" <button class={action === "delete" ? "btn-delete" : "btn-primary"} type="button" on:click={confirm} name="delete"
>{$_(action)}</button >{$_(action)}</button
> >
</div></Modal </div></Modal

View File

@@ -58,6 +58,14 @@
> >
{list.name} {list.name}
</h5> </h5>
{#if list.public}
<span
class="tooltip"
data-title={$_("public")}
>
<i class="fa fa-globe"></i>
</span>
{/if}
{#if listIsShared} {#if listIsShared}
<ShareInfo type="list" subject={list}></ShareInfo> <ShareInfo type="list" subject={list}></ShareInfo>
{/if} {/if}

View File

@@ -72,14 +72,26 @@
</script> </script>
<div class="relative"> <div class="relative">
{#if listIsShared} {#if (list.public || listIsShared) && $currentUser}
<div <div
class="absolute top-8 right-8 bg-white text-primary rounded-full w-8 py-1 text-center" class="flex absolute top-4 right-6 {list.public && listIsShared
? 'w-16'
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
> >
{#if list.public}
<span
class="tooltip"
class:mr-3={list.public && listIsShared}
data-title={$_("public")}
>
<i class="fa fa-globe"></i>
</span>
{/if}
{#if listIsShared}
<ShareInfo type="list" subject={list}></ShareInfo> <ShareInfo type="list" subject={list}></ShareInfo>
{/if}
</div> </div>
{/if} {/if}
{#if dropdownItems.length} {#if dropdownItems.length}
<div class="absolute bottom-8 right-8"> <div class="absolute bottom-8 right-8">
<Dropdown <Dropdown

View File

@@ -42,6 +42,7 @@
"completion-status": "Abschlussstatus", "completion-status": "Abschlussstatus",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"confirm-deletion": "Löschen bestätigen", "confirm-deletion": "Löschen bestätigen",
"confirm-publish": "",
"confirm-share": "Teilen bestätigen", "confirm-share": "Teilen bestätigen",
"contribute": "Mitwirken", "contribute": "Mitwirken",
"copy-link": "Link kopieren", "copy-link": "Link kopieren",
@@ -129,6 +130,7 @@
"link-copied": "Link kopiert", "link-copied": "Link kopiert",
"list": "{n, plural, =1 {Liste} other {Listen}}", "list": "{n, plural, =1 {Liste} other {Listen}}",
"list-not-shared": "Mit niemandem geteilt", "list-not-shared": "Mit niemandem geteilt",
"list-public-warning": "",
"list-saved-successfully": "Liste gespeichert", "list-saved-successfully": "Liste gespeichert",
"list-share-warning": "Durch das Teilen einer Liste werden automatisch alle darin enthaltenen Routen freigegeben.", "list-share-warning": "Durch das Teilen einer Liste werden automatisch alle darin enthaltenen Routen freigegeben.",
"list-share-warning-update": "Hinzugefügte Routen werden mit allen geteilt, die Zugriff auf diese Liste haben.", "list-share-warning-update": "Hinzugefügte Routen werden mit allen geteilt, die Zugriff auf diese Liste haben.",

View File

@@ -42,6 +42,7 @@
"completion-status": "Completion Status", "completion-status": "Completion Status",
"confirm": "Confirm", "confirm": "Confirm",
"confirm-deletion": "Confirm Deletion", "confirm-deletion": "Confirm Deletion",
"confirm-publish": "Confirm publishing",
"confirm-share": "Confirm share", "confirm-share": "Confirm share",
"contribute": "Contribute", "contribute": "Contribute",
"copy-link": "Copy Link", "copy-link": "Copy Link",
@@ -129,6 +130,7 @@
"link-copied": "Link copied!", "link-copied": "Link copied!",
"list": "{n, plural, =1 {List} other {Lists}}", "list": "{n, plural, =1 {List} other {Lists}}",
"list-not-shared": "Not shared with anyone", "list-not-shared": "Not shared with anyone",
"list-public-warning": "All trails in this list will become public.",
"list-saved-successfully": "List saved successfully", "list-saved-successfully": "List saved successfully",
"list-share-warning": "Sharing a list automatically shares all trails contained in it.", "list-share-warning": "Sharing a list automatically shares all trails contained in it.",
"list-share-warning-update": "Added trails will be shared with everyone that has access to this list.", "list-share-warning-update": "Added trails will be shared with everyone that has access to this list.",

View File

@@ -42,6 +42,7 @@
"completion-status": "L'état d'achèvement", "completion-status": "L'état d'achèvement",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "Contribuer", "contribute": "Contribuer",
"copy-link": "", "copy-link": "",
@@ -129,6 +130,7 @@
"link-copied": "", "link-copied": "",
"list": "{n, plural, =1 {Liste} other {Listes}}", "list": "{n, plural, =1 {Liste} other {Listes}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -42,6 +42,7 @@
"completion-status": "Befejezés állapota", "completion-status": "Befejezés állapota",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "Hozzájárulás", "contribute": "Hozzájárulás",
"copy-link": "", "copy-link": "",
@@ -129,6 +130,7 @@
"link-copied": "", "link-copied": "",
"list": "{n, plural, =1 {Lista} other {Listák}}", "list": "{n, plural, =1 {Lista} other {Listák}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -42,6 +42,7 @@
"completion-status": "Stato di completamento", "completion-status": "Stato di completamento",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "Contribuisci", "contribute": "Contribuisci",
"copy-link": "Copia link", "copy-link": "Copia link",
@@ -129,6 +130,7 @@
"link-copied": "Link copiato", "link-copied": "Link copiato",
"list": "{n, plural, =1 {Lista} other {Liste}}", "list": "{n, plural, =1 {Lista} other {Liste}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -42,6 +42,7 @@
"completion-status": "Voltooiingsstatus", "completion-status": "Voltooiingsstatus",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "Bijdragen", "contribute": "Bijdragen",
"copy-link": "", "copy-link": "",
@@ -129,6 +130,7 @@
"link-copied": "", "link-copied": "",
"list": "{n, plural, =1 {Lijst} other {Lijsten}}", "list": "{n, plural, =1 {Lijst} other {Lijsten}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -42,6 +42,7 @@
"completion-status": "Stan ukończenia", "completion-status": "Stan ukończenia",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "Kontrybuuj", "contribute": "Kontrybuuj",
"copy-link": "", "copy-link": "",
@@ -129,6 +130,7 @@
"link-copied": "", "link-copied": "",
"list": "{n, plural, =1 {Lista} other {Listy}}", "list": "{n, plural, =1 {Lista} other {Listy}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -42,6 +42,7 @@
"completion-status": "Status de conclusão", "completion-status": "Status de conclusão",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "Contribuir", "contribute": "Contribuir",
"copy-link": "", "copy-link": "",
@@ -129,6 +130,7 @@
"link-copied": "", "link-copied": "",
"list": "{n, plural, =1 {Lista} other {Listas}}", "list": "{n, plural, =1 {Lista} other {Listas}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -42,6 +42,7 @@
"completion-status": "完成状态", "completion-status": "完成状态",
"confirm": "", "confirm": "",
"confirm-deletion": "", "confirm-deletion": "",
"confirm-publish": "",
"confirm-share": "", "confirm-share": "",
"contribute": "贡献", "contribute": "贡献",
"copy-link": "", "copy-link": "",
@@ -129,6 +130,7 @@
"link-copied": "", "link-copied": "",
"list": "{n, plural, =1 {列表} other {列表}}", "list": "{n, plural, =1 {列表} other {列表}}",
"list-not-shared": "", "list-not-shared": "",
"list-public-warning": "",
"list-saved-successfully": "", "list-saved-successfully": "",
"list-share-warning": "", "list-share-warning": "",
"list-share-warning-update": "", "list-share-warning-update": "",

View File

@@ -5,6 +5,7 @@ import type { ListShare } from "./list_share";
export class List { export class List {
id?: string; id?: string;
name: string; name: string;
public: boolean;
description?: string; description?: string;
avatar?: string; avatar?: string;
trails?: string[]; trails?: string[];
@@ -15,8 +16,9 @@ export class List {
} }
author?: string; author?: string;
constructor(name: string, trails: Trail[], params?: { description?: string, avatar?: string, author?: string }) { constructor(name: string, trails: Trail[], params?: { description?: string, public?: boolean, avatar?: string, author?: string }) {
this.name = name; this.name = name;
this.public = params?.public ?? false
this.expand = { trails: trails }; this.expand = { trails: trails };
this.trails = trails.map(t => t.id!); this.trails = trails.map(t => t.id!);
this.description = params?.description; this.description = params?.description;

View File

@@ -20,13 +20,15 @@
trail_share_create, trail_share_create,
trail_share_index, trail_share_index,
} from "$lib/stores/trail_share_store.js"; } from "$lib/stores/trail_share_store.js";
import { trails_show } from "$lib/stores/trail_store"; import { trails_show, trails_update } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util.js"; import { getFileURL } from "$lib/util/file_util.js";
import { import {
formatDistance, formatDistance,
formatElevation, formatElevation,
formatTimeHHMM, formatTimeHHMM,
} from "$lib/util/format_util"; } from "$lib/util/format_util";
import Toggle from "$lib/components/base/toggle.svelte";
import { currentUser } from "$lib/stores/user_store.js";
export let data; export let data;
@@ -42,11 +44,36 @@
let newShares: TrailShare[] = []; let newShares: TrailShare[] = [];
let openConfirmModal: () => void; let openConfirmModal: () => void;
let openPublishConfirmModal: () => void;
const { form, errors, handleChange, handleSubmit } = createForm<List>({ const { form, errors, handleChange, handleSubmit } = createForm<List>({
initialValues: data.list!, initialValues: data.list!,
validationSchema: listSchema, validationSchema: listSchema,
onSubmit: async (submittedList) => { onSubmit: async (submittedList) => {
if (await checkPrerequisites()) {
await saveList();
}
},
});
async function checkPrerequisites() {
if (
(data.list?.public === false && $form.public === true) ||
($form.public === true &&
data.list?.expand?.trails?.length !==
$form.expand?.trails?.length)
) {
openPublishConfirmModal();
return false;
} else if (await findNewTrailShares()) {
openConfirmModal();
return false;
}
return true;
}
async function saveList() {
const avatarFile = ( const avatarFile = (
document.getElementById("avatar") as HTMLInputElement document.getElementById("avatar") as HTMLInputElement
).files![0]; ).files![0];
@@ -54,22 +81,14 @@
try { try {
if ($form.id) { if ($form.id) {
await lists_update($form, avatarFile); await lists_update($form, avatarFile);
await findNewTrailShares();
if (!newShares.length) {
show_toast({
type: "success",
icon: "check",
text: $_("list-saved-successfully"),
});
}
} else { } else {
await lists_create($form, avatarFile); await lists_create($form, avatarFile);
}
show_toast({ show_toast({
type: "success", type: "success",
icon: "check", icon: "check",
text: $_("list-saved-successfully"), text: $_("list-saved-successfully"),
}); });
}
} catch (e) { } catch (e) {
show_toast({ show_toast({
type: "error", type: "error",
@@ -79,8 +98,7 @@
} finally { } finally {
loading = false; loading = false;
} }
}, }
});
function openAvatarBrowser() { function openAvatarBrowser() {
document.getElementById("avatar")!.click(); document.getElementById("avatar")!.click();
@@ -98,22 +116,21 @@
} }
async function search(q: string) { async function search(q: string) {
const r = await fetch("/api/v1/search/multi", { const r = await fetch("/api/v1/search/trails", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
queries: [ q,
{ options: {
indexUid: "trails", filter: `author = ${$currentUser?.id} OR public = true`,
q: q, sort: ["name:desc"],
limit: 3, limit: 3,
}, },
],
}), }),
}); });
const response = await r.json(); const response = await r.json();
searchDropdownItems = response.results[0].hits searchDropdownItems = response.hits
.filter((h: List) => !$form.trails?.includes(h.id!)) .filter((h: List) => !$form.trails?.includes(h.id!))
.map((t: Record<string, any>) => ({ .map((t: Record<string, any>) => ({
text: t.name, text: t.name,
@@ -149,7 +166,10 @@
user: userId, user: userId,
}); });
for (const trail of $form.expand?.trails ?? []) { for (const trail of $form.expand?.trails ?? []) {
if (trail.author == userId) { if (
trail.author == userId ||
trail.author != $currentUser?.id
) {
continue; continue;
} }
const trailShare = existingTrailShares.find( const trailShare = existingTrailShares.find(
@@ -160,10 +180,7 @@
} }
} }
} }
return newShares.length > 0;
if (newShares.length) {
openConfirmModal();
}
} }
async function updateTrailShares() { async function updateTrailShares() {
@@ -171,11 +188,16 @@
await trail_share_create(newShare); await trail_share_create(newShare);
} }
newShares = []; newShares = [];
show_toast({ }
type: "success",
icon: "check", async function publishTrails() {
text: $_("list-saved-successfully"), for (const trail of $form.expand?.trails ?? []) {
}); if (trail.author !== $currentUser?.id) {
continue;
}
const updatedTrail: Trail = { ...trail, public: true };
await trails_update(trail, updatedTrail);
}
} }
function moveTrail(trail: Trail, index: number, direction: 1 | -1) { function moveTrail(trail: Trail, index: number, direction: 1 | -1) {
@@ -261,6 +283,8 @@
error={$errors.description} error={$errors.description}
on:change={handleChange} on:change={handleChange}
></Textarea> ></Textarea>
<Toggle name="public" label={$_("public")} bind:value={$form.public}
></Toggle>
<h3 class="text-xl font-semibold"> <h3 class="text-xl font-semibold">
{$_("trail", { values: { n: 2 } })} {$_("trail", { values: { n: 2 } })}
</h3> </h3>
@@ -379,11 +403,27 @@
</main> </main>
<ConfirmModal <ConfirmModal
id="share-confirm-modal"
text={$_("list-share-warning-update")} text={$_("list-share-warning-update")}
title={$_("confirm-share")} title={$_("confirm-share")}
action="confirm" action="confirm"
bind:openModal={openConfirmModal} bind:openModal={openConfirmModal}
on:confirm={updateTrailShares} on:confirm={async () => {
await updateTrailShares();
await saveList();
}}
></ConfirmModal>
<ConfirmModal
id="publish-confirm-modal"
text={$_("list-public-warning")}
title={$_("confirm-publish")}
action="confirm"
bind:openModal={openPublishConfirmModal}
on:confirm={async () => {
await publishTrails();
await saveList();
}}
></ConfirmModal> ></ConfirmModal>
<style> <style>