Skip to content

Commit

Permalink
feat: OPTIC-1359: Account Settings page moved to React
Browse files Browse the repository at this point in the history
  • Loading branch information
yyassi-heartex committed Dec 2, 2024
1 parent 21060c3 commit c96d83b
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 375 deletions.
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

0 comments on commit c96d83b

Please sign in to comment.