feat(frontend): fields editor

This commit is contained in:
sam 2024-11-27 19:50:45 +01:00
parent 7c52ab759c
commit f435ad4cf5
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
7 changed files with 319 additions and 166 deletions

View file

@ -2,14 +2,25 @@
import type { CustomPreference, FieldEntry } from "$api/models"; import type { CustomPreference, FieldEntry } from "$api/models";
import IconButton from "$components/IconButton.svelte"; import IconButton from "$components/IconButton.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import { InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
import FieldEntryEditor from "./FieldEntryEditor.svelte"; import FieldEntryEditor from "./FieldEntryEditor.svelte";
type Props = { type Props = {
name: string; name: string;
entries: FieldEntry[]; entries: FieldEntry[];
allPreferences: Record<string, CustomPreference>; allPreferences: Record<string, CustomPreference>;
index?: number;
move?: (index: number, up: boolean) => void;
remove?: (index: number) => void;
}; };
let { name, entries = $bindable(), allPreferences }: Props = $props(); let {
name = $bindable(),
entries = $bindable(),
allPreferences,
index,
move,
remove,
}: Props = $props();
let newEntry = $state(""); let newEntry = $state("");
@ -38,19 +49,45 @@
}; };
</script> </script>
<h4>{name}</h4> {#if index !== undefined && move && remove}
<div class="d-flex">
{#each entries as _, index} <InputGroup>
<FieldEntryEditor <IconButton
{index} icon="chevron-up"
bind:value={entries[index]} color="secondary"
{allPreferences} tooltip={$t("editor.move-field-up")}
{moveValue} onclick={() => move(index, true)}
{removeValue}
/> />
<IconButton
icon="chevron-down"
color="secondary"
tooltip={$t("editor.move-field-down")}
onclick={() => move(index, false)}
/>
<InputGroupText>{$t("editor.field-name")}</InputGroupText>
<input class="form-control" bind:value={name} />
<IconButton
color="danger"
icon="trash3"
tooltip={$t("editor.remove-field")}
onclick={() => remove(index)}
/>
</InputGroup>
</div>
{:else}
<h4>{name}</h4>
{/if}
{#each entries as _, i}
<FieldEntryEditor index={i} bind:value={entries[i]} {allPreferences} {moveValue} {removeValue} />
{/each} {/each}
<form class="input-group m-1" onsubmit={addEntry}> <form class="input-group m-1" onsubmit={addEntry}>
<input type="text" class="form-control" bind:value={newEntry} /> <input
type="text"
class="form-control"
bind:value={newEntry}
placeholder={$t("editor.new-entry")}
/>
<IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-entry")} /> <IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-entry")} />
</form> </form>

View file

@ -38,7 +38,7 @@
icon="chevron-down" icon="chevron-down"
color="secondary" color="secondary"
tooltip={$t("editor.move-entry-down")} tooltip={$t("editor.move-entry-down")}
onclick={() => moveValue(index, true)} onclick={() => moveValue(index, false)}
/> />
<input type="text" class="form-control" bind:value={value.value} /> <input type="text" class="form-control" bind:value={value.value} />
<ButtonDropdown> <ButtonDropdown>

View file

@ -0,0 +1,79 @@
<script lang="ts">
import type { RawApiError } from "$api/error";
import type { CustomPreference, Field } from "$api/models";
import IconButton from "$components/IconButton.svelte";
import { t } from "$lib/i18n";
import FieldEditor from "./FieldEditor.svelte";
import FormStatusMarker from "./FormStatusMarker.svelte";
import NoscriptWarning from "./NoscriptWarning.svelte";
type Props = {
fields: Field[];
ok: { ok: boolean; error: RawApiError | null } | null;
allPreferences: Record<string, CustomPreference>;
update: () => Promise<void>;
};
let { fields = $bindable(), ok, allPreferences, update }: Props = $props();
let newFieldName = $state("");
const moveField = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == fields.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = fields[index];
fields[index] = fields[newIndex];
fields[newIndex] = temp;
fields = [...fields];
};
const removeField = (index: number) => {
fields.splice(index, 1);
fields = [...fields];
};
const addField = (event: Event) => {
event.preventDefault();
if (!newFieldName) return;
fields = [...fields, { name: newFieldName, entries: [] }];
newFieldName = "";
};
</script>
<NoscriptWarning />
<FormStatusMarker form={ok} />
<h4>{$t("edit-profile.editing-fields-header")}</h4>
<div>
<h5>{$t("editor.add-field")}</h5>
<form class="input-group m-1" onsubmit={addField}>
<input
type="text"
class="form-control"
bind:value={newFieldName}
placeholder={$t("editor.field-name")}
/>
<IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-field")} />
</form>
</div>
{#if fields.length > 0}
<hr />
{#each fields as field, index}
<FieldEditor
{index}
bind:name={field.name}
bind:entries={field.entries}
{allPreferences}
move={moveField}
remove={removeField}
/>
{/each}
{/if}
<div>
<button class="btn btn-primary" onclick={() => update()}>{$t("save-changes")}</button>
</div>

View file

@ -133,7 +133,8 @@
"unlisted-label": "Hide from member list", "unlisted-label": "Hide from member list",
"unlisted-note": "This only hides this member from your public member list. They will still be visible to anyone at this link:", "unlisted-note": "This only hides this member from your public member list. They will still be visible to anyone at this link:",
"edit-names-pronouns-header": "Edit names and pronouns", "edit-names-pronouns-header": "Edit names and pronouns",
"back-to-profile-tab": "Back to profile" "back-to-profile-tab": "Back to profile",
"editing-fields-header": "Editing fields"
}, },
"save-changes": "Save changes", "save-changes": "Save changes",
"change": "Change", "change": "Change",
@ -145,6 +146,12 @@
"change-display-text": "Change display text", "change-display-text": "Change display text",
"display-text-example": "Optional display text (e.g. it/its)", "display-text-example": "Optional display text (e.g. it/its)",
"display-text-label": "Display text", "display-text-label": "Display text",
"display-text-info": "This is the short text shown on your profile page. If you leave it empty, it will default to the first two forms of the full set." "display-text-info": "This is the short text shown on your profile page. If you leave it empty, it will default to the first two forms of the full set.",
"move-field-up": "Move field up",
"move-field-down": "Move field down",
"remove-field": "Remove field",
"field-name": "Field name",
"add-field": "Add field",
"new-entry": "New entry"
} }
} }

View file

@ -5,6 +5,7 @@
import { mergePreferences } from "$api/models/user"; import { mergePreferences } from "$api/models/user";
import FieldEditor from "$components/editor/FieldEditor.svelte"; import FieldEditor from "$components/editor/FieldEditor.svelte";
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
import PronounsEditor from "$components/editor/PronounsEditor.svelte"; import PronounsEditor from "$components/editor/PronounsEditor.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import log from "$lib/log"; import log from "$lib/log";
@ -37,16 +38,15 @@
}; };
</script> </script>
<NoscriptWarning />
<FormStatusMarker form={ok} /> <FormStatusMarker form={ok} />
<div> <div>
<FieldEditor name={$t("profile.names-header")} bind:entries={names} {allPreferences} /> <FieldEditor name={$t("profile.names-header")} bind:entries={names} {allPreferences} />
</div> </div>
<div> <div>
<PronounsEditor bind:entries={pronouns} {allPreferences} /> <PronounsEditor bind:entries={pronouns} {allPreferences} />
</div> </div>
<div> <div>
<button class="btn btn-primary" onclick={() => update()}>{$t("save-changes")}</button> <button class="btn btn-primary" onclick={() => update()}>{$t("save-changes")}</button>
</div> </div>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { apiRequest } from "$api";
import ApiError, { type RawApiError } from "$api/error";
import { mergePreferences, type User } from "$api/models/user";
import FieldsEditor from "$components/editor/FieldsEditor.svelte";
import log from "$lib/log";
import type { PageData } from "./$types";
type Props = { data: PageData };
let { data }: Props = $props();
let fields = $state(data.user.fields);
let ok: { ok: boolean; error: RawApiError | null } | null = $state(null);
let allPreferences = $derived(mergePreferences(data.user.custom_preferences));
const update = async () => {
try {
const resp = await apiRequest<User>("PATCH", "/users/@me", {
body: { fields },
token: data.token,
});
fields = resp.fields;
ok = { ok: true, error: null };
} catch (e) {
ok = { ok: false, error: null };
log.error("Could not update fields:", e);
if (e instanceof ApiError) ok.error = e.obj;
}
};
</script>
<FieldsEditor {fields} {ok} {allPreferences} {update} />

View file

@ -4,6 +4,7 @@
import { mergePreferences, type User } from "$api/models/user"; import { mergePreferences, type User } from "$api/models/user";
import FieldEditor from "$components/editor/FieldEditor.svelte"; import FieldEditor from "$components/editor/FieldEditor.svelte";
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
import NoscriptWarning from "$components/editor/NoscriptWarning.svelte";
import PronounsEditor from "$components/editor/PronounsEditor.svelte"; import PronounsEditor from "$components/editor/PronounsEditor.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import log from "$lib/log"; import log from "$lib/log";
@ -14,9 +15,7 @@
let names = $state(data.user.names); let names = $state(data.user.names);
let pronouns = $state(data.user.pronouns); let pronouns = $state(data.user.pronouns);
let ok: { ok: boolean; error: RawApiError | null } | null = $state(null); let ok: { ok: boolean; error: RawApiError | null } | null = $state(null);
let allPreferences = $derived(mergePreferences(data.user.custom_preferences)); let allPreferences = $derived(mergePreferences(data.user.custom_preferences));
const update = async () => { const update = async () => {
@ -36,16 +35,15 @@
}; };
</script> </script>
<NoscriptWarning />
<FormStatusMarker form={ok} /> <FormStatusMarker form={ok} />
<div> <div>
<FieldEditor name={$t("profile.names-header")} bind:entries={names} {allPreferences} /> <FieldEditor name={$t("profile.names-header")} bind:entries={names} {allPreferences} />
</div> </div>
<div> <div>
<PronounsEditor bind:entries={pronouns} {allPreferences} /> <PronounsEditor bind:entries={pronouns} {allPreferences} />
</div> </div>
<div> <div>
<button class="btn btn-primary" onclick={() => update()}>{$t("save-changes")}</button> <button class="btn btn-primary" onclick={() => update()}>{$t("save-changes")}</button>
</div> </div>