fix profile page
This commit is contained in:
@@ -9,7 +9,36 @@
|
|||||||
font-size: 5rem;
|
font-size: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#avatar header.avatar_only {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 2rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#avatar header.avatar_only span.primary {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
form[name=bannerform] img.avatar {
|
form[name=bannerform] img.avatar {
|
||||||
max-width: calc(100% - 2rem);
|
max-width: calc(100% - 2rem);
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
#avatar header.avatar_only {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 2rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#avatar header.avatar_only span.primary {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|||||||
+2
-7
@@ -1,6 +1,6 @@
|
|||||||
//! Shared application state and the per-call context given to the widgets.
|
//! Shared application state and the per-call context given to the widgets.
|
||||||
|
|
||||||
use minijinja::{context, Environment, Value as JValue};
|
use minijinja::Environment;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
@@ -193,12 +193,7 @@ impl Ctx {
|
|||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let ctx = context! {
|
let ctx = crate::tpl::render_context(serde_json::to_value(&data).unwrap_or_default(), &self.lang(), &self.base_uri(), &self.jid());
|
||||||
lang => self.lang(),
|
|
||||||
base_uri => self.base_uri(),
|
|
||||||
me => self.jid(),
|
|
||||||
..JValue::from_serialize(&data)
|
|
||||||
};
|
|
||||||
match tpl.render(ctx) {
|
match tpl.render(ctx) {
|
||||||
Ok(s) => s.trim().to_string(),
|
Ok(s) => s.trim().to_string(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
+24
@@ -91,6 +91,16 @@ fn add_emojis(state: &State, s: String, no_title: Option<bool>) -> Value {
|
|||||||
Value::from_safe_string(utils::replace_emojis(&escaped, &base, no_title.unwrap_or(false)).html)
|
Value::from_safe_string(utils::replace_emojis(&escaped, &base, no_title.unwrap_or(false)).html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The widget data wins over the defaults (the Avatar templates pass their own `me`)
|
||||||
|
pub fn render_context(mut data: serde_json::Value, lang: &str, base_uri: &str, me: &str) -> Value {
|
||||||
|
if let Some(map) = data.as_object_mut() {
|
||||||
|
for (k, v) in [("lang", lang), ("base_uri", base_uri), ("me", me)] {
|
||||||
|
map.entry(k).or_insert_with(|| v.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::from_serialize(&data)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -116,4 +126,18 @@ mod tests {
|
|||||||
let html = tpl.render(data("vi")).unwrap();
|
let html = tpl.render(data("vi")).unwrap();
|
||||||
assert!(html.contains("Ghi âm") && html.contains("Tệp đã chia sẻ"));
|
assert!(html.contains("Ghi âm") && html.contains("Tệp đã chia sẻ"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_picture_header() {
|
||||||
|
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
i18n::init(&root.join("frontend"));
|
||||||
|
let env = environment(&root.join("templates"));
|
||||||
|
let tpl = env.get_template("Avatar/_avatar.html").unwrap();
|
||||||
|
let data = json!({ "me": { "picture": "/avatar/abc", "color": "yellow" } });
|
||||||
|
let html = tpl.render(render_context(data, "en", "/", "alice@example.org")).unwrap();
|
||||||
|
assert!(html.contains("url(/avatar/abc)") && html.contains("color yellow"), "{html}");
|
||||||
|
// The defaults are still there
|
||||||
|
let html = env.render_str("{{ me }} {{ lang }}", render_context(json!({}), "vi", "/", "alice@example.org")).unwrap();
|
||||||
|
assert_eq!(html, "alice@example.org vi");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
<header class="big top color {{ me.color }}" style="background-image: url({{ me.picture }})">
|
<header class="avatar_only">
|
||||||
<ul class="list thick">
|
<span class="primary icon bubble color {{ me.color }}" style="background-image: url({{ me.picture }})"
|
||||||
<li class="block">
|
onclick="Avatar_ajaxGetForm()" title="{{ __('avatar.change') }}">
|
||||||
<span class="primary icon bubble color {{ me.color }}" style="background-image: url({{ me.picture }})">
|
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<button class="button flat" onclick="Avatar_ajaxGetForm()">
|
||||||
<a class="button color transparent active" onclick="Avatar_ajaxGetForm()" title="{{ __('avatar.change') }}">
|
|
||||||
<i class="material-symbols">person_edit</i>
|
<i class="material-symbols">person_edit</i>
|
||||||
</a>
|
{{ __('avatar.change') }}
|
||||||
</div>
|
</button>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<br />
|
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Set a profile picture and check it on the profile (configuration) page."""
|
||||||
|
import asyncio, os
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
from browser import login, BASE, OUT
|
||||||
|
|
||||||
|
S = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
async def header(page):
|
||||||
|
return await page.evaluate("""() => {
|
||||||
|
const h = document.querySelector('#avatar header');
|
||||||
|
const s = document.querySelector('#avatar header span.primary');
|
||||||
|
return h ? [getComputedStyle(h).backgroundImage, s && getComputedStyle(s).backgroundImage,
|
||||||
|
h.getBoundingClientRect().height] : null;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
ctx, page, errors = await login(browser, "alice@localhost", "alicepass", "avatar")
|
||||||
|
bad = []
|
||||||
|
page.on("response", lambda r: r.status >= 400 and bad.append(f"{r.status} {r.url}"))
|
||||||
|
await page.goto(BASE + "/configuration")
|
||||||
|
await page.wait_for_timeout(4000)
|
||||||
|
print("before:", await header(page))
|
||||||
|
await page.screenshot(path=f"{OUT}/avatar_1_before.png")
|
||||||
|
|
||||||
|
await page.evaluate("Avatar_ajaxGetForm()")
|
||||||
|
await page.wait_for_selector("#dialog input[type=file]", state="attached", timeout=10000)
|
||||||
|
await page.set_input_files("#dialog input[type=file]", f"{S}/test.png")
|
||||||
|
await page.wait_for_timeout(2000)
|
||||||
|
print("photobin length:", await page.evaluate("document.querySelector('[name=photobin]').value.length"))
|
||||||
|
await page.screenshot(path=f"{OUT}/avatar_2_form.png")
|
||||||
|
await page.click("#dialog footer button:last-child")
|
||||||
|
await page.wait_for_timeout(4000)
|
||||||
|
print("after submit:", await header(page))
|
||||||
|
await page.screenshot(path=f"{OUT}/avatar_3_after.png")
|
||||||
|
|
||||||
|
await page.goto(BASE + "/configuration")
|
||||||
|
await page.wait_for_timeout(4000)
|
||||||
|
print("after reload:", await header(page))
|
||||||
|
print("nav avatar:", await page.evaluate("[...document.querySelectorAll('nav img, nav span.primary')].map(e => e.src || getComputedStyle(e).backgroundImage).slice(0, 3)"))
|
||||||
|
await page.screenshot(path=f"{OUT}/avatar_4_reload.png")
|
||||||
|
for e in errors + bad:
|
||||||
|
print(e)
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user