Skip to content

Commit

Permalink
Allow removing title of homepage (#1289)
Browse files Browse the repository at this point in the history
This removes the default homepage title and adds the options to name the
page directly, use a block name or show no name at all. That last option
is only allowed for homepages.
Though I wonder if why we don't just allow that for all pages.

Closes #1269
  • Loading branch information
LukasKalbertodt authored Dec 4, 2024
2 parents bb5060e + 0cfaef8 commit f8b7e69
Show file tree
Hide file tree
Showing 13 changed files with 71 additions and 43 deletions.
19 changes: 13 additions & 6 deletions backend/src/api/model/realm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,24 @@ impl_from_db!(

impl Realm {
pub(crate) async fn root(context: &Context) -> ApiResult<Self> {
let (selection, mapping) = select!(child_order, moderator_roles, admin_roles);
let (selection, mapping) = select!(
child_order,
moderator_roles,
admin_roles,
name,
name_from_block,
resolved_name: "realms.resolved_name",
);
let row = context.db
.query_one(&format!("select {selection} from realms where id = 0"), &[])
.await?;

Ok(Self {
key: Key(0),
parent_key: None,
plain_name: None,
resolved_name: None,
name_from_block: None,
plain_name: mapping.name.of(&row),
resolved_name: mapping.resolved_name.of(&row),
name_from_block: mapping.name_from_block.of(&row),
path_segment: String::new(),
full_path: String::new(),
index: 0,
Expand Down Expand Up @@ -322,8 +329,8 @@ impl Realm {
}

/// The raw information about the name of the realm, showing where the name
/// is coming from and if there is no name, why that is. Is `null` for the
/// root realm, non-null for all other realms.
/// is coming from and if there is no name, why that is. Can be `null` only for the
/// root realm, must be non-null for all other realms.
fn name_source(&self) -> Option<RealmNameSource> {
if let Some(name) = &self.plain_name {
Some(RealmNameSource::Plain(PlainRealmName {
Expand Down
7 changes: 5 additions & 2 deletions backend/src/api/model/realm/mutations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,11 @@ impl Realm {
realm.require_moderator_rights(context)?;

let db = &context.db;
if name.plain.is_some() == name.block.is_some() {
return Err(invalid_input!("exactly one of name.block and name.plain has to be set"));
if name.plain.is_some() && name.block.is_some() {
return Err(invalid_input!("both name.block and name.plain cannot be set"));
}
if !realm.is_main_root() && name.plain.is_none() && name.block.is_none() {
return Err(invalid_input!("exactly one of name.block and name.plain must be set for non-main-root realms"));
}
let block = name.block
.map(|id| id.key_for(Id::BLOCK_KIND)
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,4 +372,5 @@ static MIGRATIONS: Lazy<BTreeMap<u64, Migration>> = include_migrations![
37: "redo-search-triggers-and-listed",
38: "event-texts",
39: "preview-roles-and-credentials",
40: "realm-names-constraint-revision",
];
10 changes: 10 additions & 0 deletions backend/src/db/migrations/40-realm-names-constraint-revision.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Adjusts name_source constraint to allow custom names for root, including null.

alter table realms
drop constraint valid_name_source,
add constraint valid_name_source check (
-- Root is allowed to have no name.
(id = 0 and name is null or name_from_block is null)
-- All other realms have either a plain or derived name.
or (id <> 0 and (name is null) != (name_from_block is null))
);
4 changes: 1 addition & 3 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -496,11 +496,9 @@ manage:
name-from-block-description: >
Video/Serie-Element von dieser Seite verknüpfen, sodass der Seitenname
immer dem Titel des verknüpften Elements entspricht.
no-name: Keinen Namen anzeigen
no-blocks: Auf dieser Seite befinden sich keine verknüpfbaren Videos/Serien.
rename-failed: Änderung des Namen fehlgeschlagen.
no-rename-root: >
Der Name der Startseite kann nicht geändert werden. Die Überschrift wird
von der globalen Seitentitel-Einstellung kontrolliert.

children:
heading: Reihenfolge Unterseiten
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -476,11 +476,9 @@ manage:
name-from-block: Derive name from video or series
name-from-block-description: >
Link a video/series-element from this page: the page name will always be the title of the linked element.
no-name: Omit name
no-blocks: There are no linkable video/series on this page.
rename-failed: Failed to change the name.
no-rename-root: >
The homepage cannot be renamed. The heading is controlled by the global
site title setting.

children:
heading: Order of subpages
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/routes/Realm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { RootLoader } from "../layout/Root";
import { NotFound } from "./NotFound";
import { Nav } from "../layout/Navigation";
import { LinkList, LinkWithIcon } from "../ui";
import CONFIG from "../config";
import { characterClass, useTitle, useTranslatedConfig } from "../util";
import { makeRoute } from "../rauta";
import { MissingRealmName } from "./util";
Expand All @@ -27,6 +26,7 @@ import { COLORS } from "../color";
import { useMenu } from "../layout/MenuState";
import { ManageNav } from "./manage";
import { BREAKPOINT as NAV_BREAKPOINT } from "../layout/Navigation";
import CONFIG from "../config";


// eslint-disable-next-line @typescript-eslint/quotes
Expand Down Expand Up @@ -143,17 +143,16 @@ type Props = {

const RealmPage: React.FC<Props> = ({ realm }) => {
const { t } = useTranslation();
const siteTitle = useTranslatedConfig(CONFIG.siteTitle);
const breadcrumbs = realmBreadcrumbs(t, realm.ancestors);
const siteTitle = useTranslatedConfig(CONFIG.siteTitle);

const title = realm.isMainRoot ? siteTitle : realm.name;
useTitle(title, realm.isMainRoot);
useTitle(realm.name);

return <>
{!realm.isMainRoot && (
<Breadcrumbs path={breadcrumbs} tail={realm.name ?? <MissingRealmName />} />
)}
{title && (
{realm.name ? (
<div css={{
marginBottom: 20,
display: "flex",
Expand All @@ -162,9 +161,19 @@ const RealmPage: React.FC<Props> = ({ realm }) => {
columnGap: 12,
rowGap: 6,
}}>
<h1 css={{ display: "inline-block", marginBottom: 0 }}>{title}</h1>
<h1 css={{ display: "inline-block", marginBottom: 0 }}>{realm.name}</h1>
{realm.isUserRealm && <UserRealmNote realm={realm} />}
</div>
) : (
// If there is no heading, this visually hidden <h1> is added for screen readers.
realm.isMainRoot && <h1 css={{
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
whiteSpace: "nowrap",
width: 1,
}}>{siteTitle}</h1>
)}
{realm.blocks.length === 0 && realm.isMainRoot
? <WelcomeMessage />
Expand Down
36 changes: 19 additions & 17 deletions frontend/src/routes/manage/Realm/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,46 +60,40 @@ type Props = {
};

export const General: React.FC<Props> = ({ fragRef }) => {
const { t } = useTranslation();
const realm = useFragment(fragment, fragRef);

// We do not allow changing the name of the root realm.
if (realm.isMainRoot) {
return <p>{t("manage.realm.general.no-rename-root")}</p>;
}

const { nameSource, ...rest } = realm;
if (nameSource == null) {
if (!realm.isMainRoot && nameSource == null) {
return bug("name source is null for non-root realm");
}

return <NameForm realm={{ nameSource, ...rest }} />;
};

type NameFormProps = {
realm: GeneralRealmData$data & {
nameSource: NonNullable<GeneralRealmData$data["nameSource"]>;
};
realm: GeneralRealmData$data;
};

export const NameForm: React.FC<NameFormProps> = ({ realm }) => {
type FormData = {
name: string | null;
block: string | null;
nameSource: "plain-name" | "name-from-block";
nameSource: "plain-name" | "name-from-block" | "no-name";
};

const initial = {
name: realm.nameSource.__typename === "PlainRealmName"
name: realm.nameSource?.__typename === "PlainRealmName"
? realm.name
: null,
block: realm.nameSource.__typename === "RealmNameFromBlock"
block: realm.nameSource?.__typename === "RealmNameFromBlock"
// TODO: this breaks when we add new block types
? realm.nameSource.block.id ?? null
: null,
nameSource: realm.nameSource.__typename === "PlainRealmName"
? "plain-name"
: "name-from-block",
nameSource: !realm.nameSource ? "no-name" : (
realm.nameSource.__typename === "PlainRealmName"
? "plain-name"
: "name-from-block"
),
} as const;

const { t } = useTranslation();
Expand Down Expand Up @@ -135,9 +129,11 @@ export const NameForm: React.FC<NameFormProps> = ({ realm }) => {
const block = watch("block");
const nameSource = watch("nameSource");
const isPlain = nameSource === "plain-name";
const fromBlock = nameSource === "name-from-block";
const canSave = match(nameSource, {
"name-from-block": () => block != null && block !== initial.block,
"plain-name": () => !!name && name !== initial.name,
"no-name": () => initial.nameSource !== "no-name",
});

const suitableBlocks = realm.blocks
Expand Down Expand Up @@ -223,7 +219,7 @@ export const NameForm: React.FC<NameFormProps> = ({ realm }) => {
</div>
</div>
</label>
{!isPlain && <div>
{fromBlock && <div>
{suitableBlocks.length === 0 && <div>
<Card kind="error">{t("manage.realm.general.no-blocks")}</Card>
</div>}
Expand All @@ -239,6 +235,12 @@ export const NameForm: React.FC<NameFormProps> = ({ realm }) => {
</Select>}
</div>}
</div>
{realm.isMainRoot && <div>
<label>
<input type="radio" value="no-name" {...register("nameSource")} />
{t("manage.realm.general.no-name")}
</label>
</div>}
</div>

<div css={{ display: "flex", alignItems: "center" }}>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,8 @@ type Realm implements Node {
name: String
"""
The raw information about the name of the realm, showing where the name
is coming from and if there is no name, why that is. Is `null` for the
root realm, non-null for all other realms.
is coming from and if there is no name, why that is. Can be `null` only for the
root realm, must be non-null for all other realms.
"""
nameSource: RealmNameSource
"Returns `true` if this is the root of the public realm tree (with path = \"/\")."
Expand Down
1 change: 0 additions & 1 deletion frontend/tests/empty.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ test("Empty Tobira", async ({ page, browserName }) => {
await page.goto("/");

await test.step("Looks empty", async () => {
await expect(page.locator("h1").nth(0)).toContainText("Tobira Videoportal");
await expect(page.locator("main").nth(0)).toContainText("No pages yet");
expect(await page.isVisible("text='Login'")).toBe(true);
});
Expand Down
4 changes: 3 additions & 1 deletion frontend/tests/fixtures/standard.sql
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ values ('ready', '06a71e43-94cd-472d-a345-952979489e88', 'Unlisted video in seri


-- ----- Realms ---------------------------------------------------------------
-- Title for homepage:
update realms set name = 'Tobira Videoportal' where id = 0;
insert into realms (parent, path_segment, name, child_order)
values (0, 'animals', 'Animal videos', 'by_index');
insert into realms (parent, path_segment, name, child_order)
Expand Down Expand Up @@ -302,7 +304,7 @@ insert into realms (parent, path_segment, name, child_order)
values ((select id from realms where full_path = '/love'), 'turtles', 'Turtles', 'alphabetic:desc');

insert into realms (parent, path_segment, name)
values ((select id from realms where full_path = '/moon'), 'finanzamt', 'Finanzamt');
values ((select id from realms where full_path = '/moon'), 'finanzamt', 'Finanzamt');

insert into realms (parent, path_segment, name)
values ((select id from realms where full_path = '/@morgan'), 'apple', 'Apple');
Expand Down
1 change: 0 additions & 1 deletion frontend/tests/languageSelection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ test("Language selection", async ({ page }) => {
const german = page.getByRole("checkbox", { name: "Deutsch" });

await page.goto("/");
await page.waitForSelector("h1");

await test.step("Language button is present and opens menu", async () => {
await page.getByRole("button", { name: "Language selection" }).click();
Expand Down
2 changes: 1 addition & 1 deletion util/dummy-realms.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# A realm tree definition for development. Can be imported with
# `cargo run -- import-realm-tree dummy-realms.yaml`

name: ""
name: "Tobira Videoportal"
path: ""
blocks:
- text: |
Expand Down

0 comments on commit f8b7e69

Please sign in to comment.