Skip to content
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: OPTIC-1359: Account Settings page moved to React #6741

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
372 changes: 0 additions & 372 deletions label_studio/users/templates/users/user_account.html
Original file line number Diff line number Diff line change
@@ -1,374 +1,2 @@
{% extends 'base.html' %}
{% load static %}

{% block head %}
<link rel="stylesheet" href="{{ settings.HOSTNAME }}{% static 'css/uikit.css' %}">
<link rel="stylesheet" href="{{ settings.HOSTNAME }}{% static 'css/users.css' %}">
{% endblock %}

{% block divider %}
{% endblock %}

{% block frontend_settings %}
{
breadcrumbs: [
{
title: "Account & Settings"
}
],
}
{% endblock %}

{% block content %}
<script>
// Take closest form (or form by id) and submit it,
// optional: prevent page refresh if done passed as function
function smart_submit(done, form_id) {
// use form id or take closest form to the event target
var f = typeof form_id === 'string' ? $(form_id) : $(event.target).closest('form');
console.log('smart_submit found form to use', f);

if (typeof f === 'undefined') {
console.log('smart_submit event target', event.target);
alert("Closest form not found for smart_submit")
}
console.log('YYYYYYY' + $(f).serialize());
var params = {
url: $(f).attr('action'),
type: $(f).attr('method'),
data: $(f).serialize(),

error: function (result, textStatus, errorThrown) {
console.log('smart_submit ajax error', result);

// call done function if it's defined and ignore all the rest
if (typeof done === "function") {
done(result)
}
// default error handle
else {
if (typeof result.responseText !== 'undefined') {
alert("Error: " + message_from_response(result));
} else {
alert("Request error: " + errorThrown);
}
window.location.reload();
}
},

success: function (data, textStatus, result) {
// call done function if it's defined and ignore all the rest
if (typeof done === "function") {
done(result)
}
// default success handle
else {
window.location.reload();
}
}
};

console.log('smart_submit ajax params', params);
$.ajax(params);
event.preventDefault();
return false;
}

function message_from_response(result) {
console.log(result);

// result is dict
if (result.hasOwnProperty('detail') && result.detail)
return result.detail;

// result is object from XHR, check responseText first, it is always presented
if (!result.responseText)
return 'Critical error on server';

// grab responseJSON detail
else if (result.responseJSON && result.responseJSON.hasOwnProperty('detail'))
return result.responseJSON['detail'];

// something strange inside of responseJSON
else if (result.responseJSON)
return JSON.stringify(result.responseJSON);

else
return JSON.stringify(result)
}
</script>

<div class="full_content">
<div class="account-page">
<form action="{% url 'user-detail' pk=user.pk %}" method="patch" class="user__info">
<input type="hidden" name="_method" value="patch"/>

<header>Account info</header>
<ul>
<li class="field">
<label for="">E-mail</label>
<input type="text" class="lsf-input-ls" value="{{user.email}}" disabled />
</li>
<li class="field">
<label for="">First Name</label>
<input type="text" class="lsf-input-ls" name="first_name" value="{{user.first_name}}" />
</li>
<li class="field">
<label for="">Last Name</label>
<input type="text" class="lsf-input-ls" name="last_name" value="{{user.last_name}}" />
</li>
<li class="field">
<label for="">Phone</label>
<input type="text" class="lsf-input-ls" name="phone" value="{{user.phone}}" />
<!-- <span>We'll send you sms with code if you change your number</span>
<span class="error">Incorrect phone number!</span> -->
</li>
</ul>
<div class="user-some-actions">

<div class="user-pic {{ user.avatar|yesno:'can_delete,can_upload' }}">
<div class="userpic userpic--big">
{% if user.avatar %}
<img src="{{user.avatar_url}}" alt="User photo" width="92" />
{% endif %}

{% if user.get_initials %}
<span>{{user.get_initials}}</span>
{% else %}
<span>{{user.username}}</span>
{% endif %}
</div>

<button class="lsf-button-ls lsf-button-ls_look_danger" name="delete-avatar" type="button">
Delete
</button>

<input class="file-input" type="file" name="avatar" value="Choose"
accept="image/png, image/jpeg, image/jpg"/>
</div>

<!-- <div class="user-activity">
<p>Inspect all your actions<br/>performed on the platform</p>
<button type="button">Activity Log<img src="" /></button>
</div> -->
</div>
<footer>
<p class="secondary">Registered {{ user.date_joined|date:"M j, Y" }}, user ID {{ user.id }}</p>
<button class="lsf-button-ls lsf-button-ls_look_primary" onclick="smart_submit()">Save</button>
</footer>
</form>

<!-- Token -->
<form action="" class="access_token__info">
<header>Access Token</header>
<div class="field field--wide">
<label for="access_token">Use this token to authenticate with our API:</label>
<input id="access_token" class="lsf-input-ls" name="access_token" type="text" value="{{token}}" readonly />
<p class="actions">
<button type="button" class="blinking-status lsf-button-ls" data-copy="access_token">Copy</button>
<button type="button" class="blinking-status lsf-button-ls" name="renew">Renew</button>
</p>
</div>
<!-- Example -->
<div class="field field--wide">
<label for="example_fetch">Example fetch projects data:</label>
<textarea id="example_fetch" class="example_code ls-textarea" type="text" readonly
style="height: 92px; font-size: 14px">
{% if settings.HOSTNAME %}
curl -X GET {{ settings.HOSTNAME }}/api/projects/ -H 'Authorization: Token {{token}}'
{% else %}
curl -X GET {{ request.scheme }}://{{ request.get_host }}/api/projects/ -H 'Authorization: Token {{token}}'
{% endif %}
</textarea>
<p class="actions">
<button type="button" class="blinking-status lsf-button-ls" data-copy="example_fetch">Copy</button>

{% block api_docs %}
<a
class="lsf-button-ls"
href="https://labelstud.io/guide/api.html"
target="_blank"
>
Documentation
</a>
{% endblock %}
</p>
</div>
</form>


<!-- Organization -->
<form action="" class="organization block-info" id="organization">
<header>
{{ user.active_organization.title }}
<br>
<sub>Your active organization</sub>
</header>

<table>
{% with user.get_pretty_role as role %}
{% if role %}
<tr><td>Your role</td><td>{{ user.get_pretty_role }}</td></tr>
{% endif %}
{% endwith %}
<tr><td>Annotations completed by you</td><td>{{ user.active_organization_annotations.count }}</td></tr>
<tr><td>Projects contributed by you</td><td>{{ user.active_organization_contributed_project_number }}</td></tr>
<tr><td></td><td></td></tr>
<tr><td style="min-width: 75px">Organization ID</td><td>{{ user.active_organization.id }}</td></tr>
<tr><td>Organization owner</td><td>{{ user.active_organization.created_by }}</td></tr>
<tr><td>Organization created at</td><td>{{ user.active_organization.created_at }}</td></tr>
</table>

</form>

<!-- Notifications -->
{% block notifications %}
<form action="{% url 'user-detail' pk=user.pk %}?update-notifications=1" method="patch" class="notifications block-info" id="notifications">
<header>
Notifications
<br>
<sub>Email and other notifications</sub>
</header>

<table>
<tr><td style="{% if user.allow_newsletters is None %}border: 1px red solid; border-radius: 5px{% endif %}">

<input name="allow_newsletters" type="hidden"
value="{% if user.allow_newsletters is None %}true{% else %}{{ user.allow_newsletters|yesno:"false,true" }}{% endif %}">

<input name="allow_newsletters_visual" id="allow_newsletters_visual" type="checkbox"
style="width: auto;"
{% if user.allow_newsletters %}checked="true"{% endif %}
onclick="smart_submit()">

<label for="allow_newsletters_visual" style="display: inline-block; cursor: pointer; margin-top: -10px">
Get the latest news & tips from Heidi
<img src="{{ settings.HOSTNAME }}{% static 'images/heidi.png' %}" alt="Heidi"
width="25" style="display: inline; margin: 0; position: relative; top: 5px; left: 0">
</label>

</td></tr>
</table>

</form>
{% endblock %}


</div>

<script>
(() => {
{% if settings.HOSTNAME %}
const hostname = '{{ settings.HOSTNAME }}';
{% else %}
const hostname = '{{ request.scheme }}://{{ request.get_host }}';
{% endif %}

document.querySelectorAll('[data-copy]').forEach(button => {
button.onclick = e => {
const input = document.getElementById(e.target.dataset.copy);
input.select();
document.execCommand("copy");
input.setSelectionRange(0, 0);
input.blur();
button.classList.add('blink');
setTimeout(() => button.classList.remove('blink'))
}
});

document.querySelector('[name=renew]').onclick = e => {
const button = e.target;
const input = document.getElementById("access_token");
const example = document.getElementById("example_fetch");

fetch("{% url 'current-user-reset-token' %}", { method: "POST" })
.then(res => res.json())
.then(res => {
input.value = res.token;
example.value = `curl -X GET ${hostname}/api/projects/ -H 'Authorization: Token ${res.token}'`
button.classList.add('blink');
setTimeout(() => button.classList.remove('blink'))
});
};

$('[name=avatar]').on('change', async (e) => {
const formData = new FormData;

formData.append(e.target.name, e.target.files[0]);

try {
const response = await fetch("{% url 'user-avatar' pk=user.pk %}", {
method: "post",
body: formData
});

if (!response.ok) {
handleResponseError(response)
} else {
updateAvatar(true)
}
} catch (err) {
console.error(err)
}
});

$('[name=delete-avatar]').on('click', async (e) => {
try {
const response = await fetch("{% url 'user-avatar' pk=user.pk %}", {
method: "delete"
})

if (!response.ok) {
handleResponseError(response)
} else {
updateAvatar(false)
}
} catch (err) {
console.err(err)
}
})

/**
* @param {Response} response
*/
const handleResponseError = (response) => {
response.json().then(data => {
alert(message_from_response(data));
})
}

const updateAvatar = async (setAvatar = true) => {
if (setAvatar) {
const response = await fetch("{% url 'current-user-whoami' %}")

if (response.ok) {
const {avatar} = await response.json()
const userpic = document.querySelector('.userpic')

let userpicImage = userpic.querySelector('img')

if (!userpicImage) {
userpicImage = document.createElement('img')
userpic.insertBefore(userpicImage, userpic.firstChild);
}

userpicImage.src = avatar

const userpicRoot = document.querySelector('.user-pic');
userpicRoot.classList.remove('can_delete', 'can_upload')
userpicRoot.classList.add('can_delete')
}
} else {
const userpic = document.querySelector('.user-pic')
const userpicImage = userpic.querySelector('img')
if (userpicImage) userpicImage.remove();

userpic.classList.remove('can_delete', 'can_upload')
userpic.classList.add('can_upload')
}
}
})();
</script>
</div>

{% endblock %}
2 changes: 2 additions & 0 deletions web/apps/labelstudio/src/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import ErrorBoundary from "./ErrorBoundary";
import { RootPage } from "./RootPage";
import { FF_OPTIC_2, FF_UNSAVED_CHANGES, isFF } from "../utils/feature-flags";
import { ToastProvider, ToastViewport } from "@humansignal/ui";
import { CurrentUserProvider } from "../providers/CurrentUser";

const baseURL = new URL(APP_SETTINGS.hostname || location.origin);
export const UNBLOCK_HISTORY_MESSAGE = "UNBLOCK_HISTORY";
Expand Down Expand Up @@ -61,6 +62,7 @@ const App = ({ content }) => {
<RoutesProvider key="rotes" />,
<ProjectProvider key="project" />,
<ToastProvider key="toast" />,
<CurrentUserProvider key="current-user" />,
]}
>
<AsyncPage>
Expand Down
1 change: 1 addition & 0 deletions web/apps/labelstudio/src/assets/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ export { ReactComponent as IconSparkGrey } from "./spark-grey.svg";
export { ReactComponent as IconPredictions } from "./predictions.svg";
export { ReactComponent as IconEmptyPredictions } from "./empty-predictions.svg";
export { ReactComponent as IconSpark } from "./spark.svg";
export { ReactComponent as IconLaunch } from "./launch.svg";
export { ReactComponent as IconModel } from "./model.svg";
export { ReactComponent as IconModels } from "./models.svg";
Loading
Loading