-
Notifications
You must be signed in to change notification settings - Fork 378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Site status message #7659
Changes from 9 commits
d03fd2e
933f0be
04ab0ba
175aea5
6408b65
4f47554
cd5f9ae
1a338bb
d6b2eee
7e98341
5c10968
42ed840
5bc8145
174fdd8
7a0c02b
d207c1d
b64d267
3423355
ad8d265
93c986e
7368ae1
09c87c0
9db7147
93eb0f0
2c9c897
dfa3732
b21661c
04cef0f
a7e627f
de8a991
e7ef7f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Various uses of semi-colons. Remove all. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<script> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
import { h, defineComponent } from 'vue' | ||
import { useNotification } from 'naive-ui' | ||
import { localStorageWrapper } from '../shared/local-storage-wrapper'; | ||
import { JSONWrapper } from '../shared/json-wrapper'; | ||
import { STATUS_STORAGE_KEY, generateStatusTestId } from '../shared/status-common' | ||
|
||
const getDismissedStatuses = () => { | ||
const jsonString = localStorageWrapper.getItem(STATUS_STORAGE_KEY) | ||
const jsonValue = JSONWrapper.parse(jsonString, []) | ||
if(Array.isArray(jsonValue)) { | ||
return jsonValue; | ||
} | ||
return []; | ||
} | ||
|
||
const dismissStatus = (id) => { | ||
const dissmissed = [id, ...getDismissedStatuses()]; | ||
localStorageWrapper.setItem(STATUS_STORAGE_KEY, JSONWrapper.stringify(dissmissed)); | ||
return true; | ||
} | ||
|
||
let notificationInstances = {} // keyed by status.id | ||
let notification | ||
|
||
const pollStatusAPI = () => { | ||
fetch('/status/latest.json') | ||
.then(resp => resp.json()) | ||
.then(status => { | ||
if(status === null || status.hasMessage === false) { | ||
console.info("No status message") | ||
return | ||
} | ||
const dismissedStatuses = getDismissedStatuses() | ||
if(dismissedStatuses.includes(status.id)) { | ||
console.info(`Not showing site status ${status.id} because it was already dismissed. Dismissed Ids:`, dismissedStatuses) | ||
return | ||
} | ||
|
||
const isSameStatusPage = Boolean(document.querySelector(`[data-status-id="${status.id}"]`)) | ||
|
||
if(isSameStatusPage) { | ||
console.info(`Not showing site status ${status.id} because we're on the target page`) | ||
return | ||
} | ||
|
||
if(notificationInstances[status.id]) { | ||
console.info(`Not showing site status ${status.id} because it's already been displayed`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change to console.debug? |
||
return | ||
} | ||
|
||
notificationInstances[status.id] = notification.create({ | ||
title: status.title, | ||
content: status.body, | ||
meta: `${status.date}`, | ||
action: () => | ||
h( | ||
'a', | ||
{ | ||
'data-testid': generateStatusTestId(status.id), | ||
href: status.url, | ||
'aria-label': `Read more about ${status.title}` | ||
}, | ||
"Read more" | ||
), | ||
onClose: () => { | ||
return dismissStatus(status.id) | ||
} | ||
}) | ||
}) | ||
.catch(e => { | ||
console.error(e) | ||
}) | ||
} | ||
|
||
|
||
export default defineComponent({ | ||
setup() { | ||
notification = useNotification() | ||
pollStatusAPI(notification) | ||
} | ||
}) | ||
</script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const JSONWrapper = { | ||
parse(jsonString, defaultValue) { | ||
if(typeof jsonString !== "string") return defaultValue | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid single line if / return |
||
try { | ||
return JSON.parse(jsonString); | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
return defaultValue | ||
}, | ||
stringify(data) { | ||
try { | ||
return JSON.stringify(data); | ||
} catch (e) { | ||
console.error(e) | ||
} | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
|
||
/* | ||
* DEVELOPER NOTE | ||
* | ||
* Some browsers can block storage (localStorage, sessionStorage) | ||
* access for privacy reasons, and all browsers can have storage | ||
* that's full, and then they throw exceptions. | ||
* | ||
* See https://michalzalecki.com/why-using-localStorage-directly-is-a-bad-idea/ | ||
* | ||
* Exceptions can even be thrown when testing if localStorage | ||
* even exists. This can throw: | ||
* | ||
* if (window.localStorage) | ||
* | ||
* Also localStorage/sessionStorage can be enabled after DOMContentLoaded | ||
* so we handle it gracefully. | ||
* | ||
* 1) we need to wrap all usage in try/catch | ||
* 2) we need to defer actual usage of these until | ||
* necessary, | ||
* | ||
*/ | ||
|
||
export const localStorageWrapper = { | ||
getItem: (key) => { | ||
try { | ||
return localStorage.getItem(key) | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
return null; | ||
}, | ||
setItem: (key, value) => { | ||
try { | ||
return localStorage.setItem(key, value) | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
return; | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Used in Playwright Status and components | ||
|
||
export const STATUS_STORAGE_KEY = "status-dismissed" | ||
|
||
export const generateStatusTestId = (id) => `status-${id}` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from datetime import datetime | ||
from django.contrib import admin | ||
from django.template.defaultfilters import slugify | ||
from .models import Status | ||
|
||
class StatusAdmin(admin.ModelAdmin): | ||
list_display = ['title', 'body', 'active', 'date', 'by', 'page'] | ||
raw_id_fields = ['by'] | ||
|
||
def get_changeform_initial_data(self, request): | ||
date = datetime.now() | ||
return { | ||
"slug": slugify(f"{date.year}-{date.month}-{date.day}-"), | ||
} | ||
|
||
admin.site.register(Status, StatusAdmin) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class StatusConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "ietf.status" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Generated by Django 4.2.13 on 2024-06-24 04:24 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace the Generated by with the trust copyright line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, since this is a new app coming in, delete the migrations and regenerate them so that there's only a 0001_initial. |
||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
("person", "0002_alter_historicalperson_ascii_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="Status", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
("date", models.DateField()), | ||
( | ||
"active", | ||
models.BooleanField( | ||
default=True, help_text="Only active messages will be shown" | ||
), | ||
), | ||
( | ||
"message", | ||
models.CharField(help_text="Your status message.", max_length=255), | ||
), | ||
("url", models.URLField()), | ||
( | ||
"by", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, to="person.person" | ||
), | ||
), | ||
], | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Generated by Django 4.2.13 on 2024-06-30 21:51 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("status", "0001_initial"), | ||
] | ||
|
||
operations = [ | ||
migrations.RenameField( | ||
model_name="status", | ||
old_name="message", | ||
new_name="body", | ||
), | ||
migrations.RemoveField( | ||
model_name="status", | ||
name="url", | ||
), | ||
migrations.AddField( | ||
model_name="status", | ||
name="page", | ||
field=models.TextField(blank=True, null=True), | ||
), | ||
migrations.AddField( | ||
model_name="status", | ||
name="slug", | ||
field=models.SlugField(blank=True, null=True), | ||
), | ||
migrations.AddField( | ||
model_name="status", | ||
name="status_id", | ||
field=models.IntegerField(null=True), | ||
), | ||
migrations.AddField( | ||
model_name="status", | ||
name="title", | ||
field=models.CharField(default="", max_length=255), | ||
preserve_default=False, | ||
), | ||
migrations.AlterField( | ||
model_name="status", | ||
name="date", | ||
field=models.DateTimeField(auto_now_add=True), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Generated by Django 4.2.13 on 2024-06-30 21:52 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("status", "0002_rename_message_status_body_remove_status_url_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.RemoveField( | ||
model_name="status", | ||
name="id", | ||
), | ||
migrations.AlterField( | ||
model_name="status", | ||
name="status_id", | ||
field=models.IntegerField(primary_key=True, serialize=False), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Generated by Django 4.2.13 on 2024-07-01 01:07 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("status", "0003_remove_status_id_alter_status_status_id"), | ||
] | ||
|
||
operations = [ | ||
migrations.RemoveField( | ||
model_name="status", | ||
name="status_id", | ||
), | ||
migrations.AddField( | ||
model_name="status", | ||
name="id", | ||
field=models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
preserve_default=False, | ||
), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No dangling comma