feat(frontend): fields editor
This commit is contained in:
parent
7c52ab759c
commit
f435ad4cf5
7 changed files with 319 additions and 166 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue