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

Differentiate display from user ID and improve user selection #23

Closed
simonw opened this issue Sep 2, 2024 · 13 comments
Closed

Differentiate display from user ID and improve user selection #23

simonw opened this issue Sep 2, 2024 · 13 comments
Labels
enhancement New feature or request

Comments

@simonw
Copy link
Contributor

simonw commented Sep 2, 2024

The autocomplete from this issue:

Isn't fit for purpose on Datasette Cloud, where user IDs are integers.

Need to be able to display their "display names" in lists of e.g. members of a group, and also autocomplete against those when adding users to groups or to table permissions.

Also: the <datalist> autocomplete really isn't very good - it still allows freeform text input and, at least on Firefox, shows a whole butch of irrelevant suggestions mixed in with the "valid" options:

CleanShot 2024-09-02 at 11 57 06@2x

@simonw simonw added the enhancement New feature or request label Sep 2, 2024
@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

I can use the actors_from_ids mechanism to show better actors, and I can update the design of the datasette_acl_actor_ids plugin hook to return whole actors, not just IDs, and implement one of the JavaScript autocomplete things I considered in #18 (comment)

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

From https://alphagov.github.io/accessible-autocomplete/#progressive-enhancement

If your autocomplete is meant to select from a small list of options (a few hundred), we strongly suggest that you render a <select> menu on the server, and use progressive enhancement.

Instances with more than a few hundred users will be rare, I'm going to do that.

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

I tried getting the alphagov one working - it's pretty glitchy for me:

CleanShot 2024-09-02 at 12 39 02@2x

I had to hack the source code and add the selected text here to disable the 1Password icon on it too:

CleanShot 2024-09-02 at 12 39 43@2x

Here's my prototype:

diff --git a/datasette_acl/templates/manage_acl_group.html b/datasette_acl/templates/manage_acl_group.html
index b293f42..529b6e8 100644
--- a/datasette_acl/templates/manage_acl_group.html
+++ b/datasette_acl/templates/manage_acl_group.html
@@ -3,6 +3,8 @@
 {% block title %}{{ name }}{% endblock %}
 
 {% block extra_head %}
+<script src="{{ urls.static_plugins("datasette-acl", "accessible-autocomplete.min.js") }}"></script>
+<link rel="stylesheet" href="{{ urls.static_plugins("datasette-acl", "accessible-autocomplete.min.css") }}">
 <style>
 .remove-button {
   background-color: #fff;
@@ -75,14 +77,22 @@
 
 <form action="{{ request.path }}" method="post">
   <input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
-  <p><label>User ID <input type="text" data-1p-ignore name="add"{% if valid_actor_ids %} list="actor-ids"{% endif %}></label> <input type="submit" value="Add member"></p>
-  {% if valid_actor_ids %}
-    <datalist id="actor-ids">{% for actor_id in valid_actor_ids %}
-      <option value="{{ actor_id }}"></option>
+  <p><label for="id_add">User ID</label> <select id="id_add" name="add">
+    {% for actor_id in valid_actor_ids %}
+      <option>{{ actor_id }}</option>
     {% endfor %}
-    </datalist>
-  {% endif %}
+  </select>
 </form>
+
+
+<script>
+const userSelect = document.querySelector('#id_add');
+
+accessibleAutocomplete.enhanceSelectElement({
+  selectElement: userSelect
+});
+document.getElementById('add').setAttribute('data-1p-ignore', '');
+</script>
 {% endif %}
 {% endif %}

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

Got this working with https://projects.verou.me/awesomplete/

muppets

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

I'd prefer it if selecting the item submitted the form, you have to enter twice right now.

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

Here's that prototype so far (the custom event thing doesn't work though):

diff --git a/datasette_acl/templates/manage_acl_group.html b/datasette_acl/templates/manage_acl_group.html
index b293f42..9bbf7f3 100644
--- a/datasette_acl/templates/manage_acl_group.html
+++ b/datasette_acl/templates/manage_acl_group.html
@@ -3,6 +3,8 @@
 {% block title %}{{ name }}{% endblock %}
 
 {% block extra_head %}
+<script src="{{ urls.static_plugins("datasette-acl", "awesomplete.min.js") }}"></script>
+<link rel="stylesheet" href="{{ urls.static_plugins("datasette-acl", "awesomplete.css") }}">
 <style>
 .remove-button {
   background-color: #fff;
@@ -75,7 +77,7 @@
 
 <form action="{{ request.path }}" method="post">
   <input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
-  <p><label>User ID <input type="text" data-1p-ignore name="add"{% if valid_actor_ids %} list="actor-ids"{% endif %}></label> <input type="submit" value="Add member"></p>
+  <p><label>User ID <input id="id_add" type="text" data-minchars="1" data-1p-ignore name="add"{% if valid_actor_ids %} list="actor-ids"{% endif %}></label> <input type="submit" value="Add member"></p>
   {% if valid_actor_ids %}
     <datalist id="actor-ids">{% for actor_id in valid_actor_ids %}
       <option value="{{ actor_id }}"></option>
@@ -118,10 +120,16 @@
 {% endif %}
 
 <script>
-// Focus on add input if we just added a member
-if (window.location.hash === '#focus-add') {
-  document.querySelector('input[name="add"]').focus();
-}
+document.addEventListener('DOMContentLoaded', function() {
+  document.querySelector('#id_add').addEventListener('awesomplete-select', (ev) => {
+    console.log(ev);
+    // this.closest('form').submit();
+  });
+  // Focus on add input if we just added a member
+  if (window.location.hash === '#focus-add') {
+    document.querySelector('input[name="add"]').focus();
+  }
+});
 </script>
 
 {% endblock %}

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

I'm going to try https://choices-js.github.io/Choices/

@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

I like Choices best:

choices

@simonw simonw added this to the Feature complete milestone Sep 2, 2024
@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

That prototoype so far:

diff --git a/datasette_acl/templates/manage_acl_group.html b/datasette_acl/templates/manage_acl_group.html
index b293f42..12b1c0b 100644
--- a/datasette_acl/templates/manage_acl_group.html
+++ b/datasette_acl/templates/manage_acl_group.html
@@ -3,6 +3,8 @@
 {% block title %}{{ name }}{% endblock %}
 
 {% block extra_head %}
++<script src="{{ urls.static_plugins("datasette-acl", "choices-9.0.1.min.js") }}"></script>
++<link rel="stylesheet" href="{{ urls.static_plugins("datasette-acl", "choices-9.0.1.min.css") }}">
 <style>
 .remove-button {
   background-color: #fff;
@@ -75,13 +77,17 @@
 
 <form action="{{ request.path }}" method="post">
   <input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
-  <p><label>User ID <input type="text" data-1p-ignore name="add"{% if valid_actor_ids %} list="actor-ids"{% endif %}></label> <input type="submit" value="Add member"></p>
-  {% if valid_actor_ids %}
-    <datalist id="actor-ids">{% for actor_id in valid_actor_ids %}
-      <option value="{{ actor_id }}"></option>
-    {% endfor %}
-    </datalist>
-  {% endif %}
+  <div style="display: flex; align-items: center; gap: 10px; max-width: 500px">
+    <label for="id_add" style="flex-shrink: 0;">User ID</label>
+    <div class="choices" data-type="select-one" tabindex="0" style="flex-grow: 1;">
+      <select id="id_add" name="add">
+        <option></option>
+        {% for actor_id in valid_actor_ids %}
+          <option>{{ actor_id }}</option>
+        {% endfor %}
+      </select>
+    </div>
+  </div>
 </form>
 {% endif %}
 {% endif %}
@@ -118,10 +124,17 @@
 {% endif %}
 
 <script>
-// Focus on add input if we just added a member
-if (window.location.hash === '#focus-add') {
-  document.querySelector('input[name="add"]').focus();
-}
+document.addEventListener('DOMContentLoaded', function() {
+  const select = document.querySelector('#id_add');
+  const choices = new Choices(select);
+  select.addEventListener('addItem', (ev) => {
+    ev.target.closest('form').submit()
+  });
+  // Focus on add input if we just added a member
+  if (window.location.hash === '#focus-add') {
+    choices.showDropdown();
+  }
+});
 </script>
 
 {% endblock %}

@simonw simonw changed the title Differentiate display from user ID in autocomplete Differentiate display from user ID and improve user selection Sep 2, 2024
@simonw
Copy link
Contributor Author

simonw commented Sep 2, 2024

Claude artifact showing what it could look like if I use this rather than the table of checkboxes:

https://claude.site/artifacts/3b83782b-74d3-4759-ac68-523fe2a905eb

CleanShot 2024-09-02 at 16 14 09@2x

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Group Permissions UI</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/choices.js/10.2.0/choices.min.css">
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f0f0f0;
        }
        .container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        h1 {
            color: #333;
        }
        .group-row {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border-radius: 4px;
        }
        .group-name {
            width: 150px;
            font-weight: bold;
            color: #4a4a4a;
        }
        select[multiple] {
            min-width: 200px;
        }
        /* Choices.js custom styles */
        .choices__inner {
            min-height: 30px;
            padding: 4px 7.5px 4px 3.75px;
        }
        .choices__list--multiple .choices__item {
            font-size: 12px;
            padding: 2px 5px;
            margin-bottom: 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Group Permissions</h1>
        <div id="groups-container">
            <div class="group-row">
                <span class="group-name">staff (1)</span>
                <select multiple id="select-staff">
                    <option value="insert-row">insert-row</option>
                    <option value="delete-row">delete-row</option>
                    <option value="update-row">update-row</option>
                    <option value="alter-table" selected>alter-table</option>
                    <option value="drop-table">drop-table</option>
                </select>
            </div>
            <div class="group-row">
                <span class="group-name">devs (5)</span>
                <select multiple id="select-devs">
                    <option value="insert-row" selected>insert-row</option>
                    <option value="delete-row" selected>delete-row</option>
                    <option value="update-row" selected>update-row</option>
                    <option value="alter-table" selected>alter-table</option>
                    <option value="drop-table">drop-table</option>
                </select>
            </div>
            <div class="group-row">
                <span class="group-name">newgroup (0)</span>
                <select multiple id="select-newgroup">
                    <option value="insert-row">insert-row</option>
                    <option value="delete-row">delete-row</option>
                    <option value="update-row">update-row</option>
                    <option value="alter-table" selected>alter-table</option>
                    <option value="drop-table">drop-table</option>
                </select>
            </div>
            <div class="group-row">
                <span class="group-name">muppets (5)</span>
                <select multiple id="select-muppets">
                    <option value="insert-row">insert-row</option>
                    <option value="delete-row">delete-row</option>
                    <option value="update-row">update-row</option>
                    <option value="alter-table">alter-table</option>
                    <option value="drop-table">drop-table</option>
                </select>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/choices.js/10.2.0/choices.min.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const selects = document.querySelectorAll('select[multiple]');
            selects.forEach(select => {
                new Choices(select, {
                    removeItemButton: true,
                    classNames: {
                        containerOuter: 'choices custom-choices',
                    }
                });
            });
        });
    </script>
</body>
</html>

Conversation transcript: https://gist.github.com/simonw/7b87b24cd53daf8ea05170c3c8013e3c

@simonw
Copy link
Contributor Author

simonw commented Sep 10, 2024

I implemented Choices for permission selection and user selection here:

Still need to differentiate user ID from user display though.

@simonw
Copy link
Contributor Author

simonw commented Sep 10, 2024

New datasette_acl_actor_ids hook design: it can return a list of IDs, or it can return a list of dicts with "id" and "display" keys.

@simonw
Copy link
Contributor Author

simonw commented Sep 10, 2024

I'm going to rename datasette_acl_actor_ids to datasette_acl_valid_actors.

@simonw simonw closed this as completed in 37325c7 Sep 10, 2024
simonw added a commit that referenced this issue Sep 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant