Adds share feature
This commit is contained in:
parent
287c1f67c5
commit
15a9dcf09b
14 changed files with 379 additions and 23 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
|||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap5-autocomplete": "^1.1.33",
|
||||
"datatables.net-bs5": "^2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -699,6 +700,12 @@
|
|||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap5-autocomplete": {
|
||||
"version": "1.1.33",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.33.tgz",
|
||||
"integrity": "sha512-VgHSx2hCNEBThFzb57HziDA2BNuc0wT5+V9XqIbXsV6oKYXcyRE2ytFIJcHjTCEIYqTsNCFiCQILIXc3YANGPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap5-autocomplete": "^1.1.33",
|
||||
"datatables.net-bs5": "^2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,24 +1,130 @@
|
|||
import DataTable from "datatables.net-bs5";
|
||||
import DataTable, {Api} from "datatables.net-bs5";
|
||||
// @ts-expect-error
|
||||
import Autocomplete from "bootstrap5-autocomplete";
|
||||
import {getCurrentlySelectedList} from "./userlists";
|
||||
|
||||
const SEARCH_API_URL = '/api/web/share/search';
|
||||
const ADD_API_URL = '/api/web/share/add';
|
||||
const REMOVE_API_URL = '/api/web/share/remove';
|
||||
|
||||
const DT_API_URL = '/api/dt/list/users';
|
||||
|
||||
let dtApi: Api;
|
||||
|
||||
function actionColumnRender(
|
||||
id: number, type: string
|
||||
) {
|
||||
if (type !== 'display') {
|
||||
return id;
|
||||
}
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.classList.add('fa-solid', 'fa-xmark', 'text-danger', 'cursor-pointer', 'fa-xl');
|
||||
icon.addEventListener('click', async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('requestedUser', id.toString() ?? '');
|
||||
formData.append('listid', getCurrentlySelectedList()?.toString() ?? '');
|
||||
|
||||
const response = await fetch(
|
||||
REMOVE_API_URL,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
dtApi.ajax.reload();
|
||||
})
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
export function initShare() {
|
||||
const input = document.querySelector<HTMLInputElement>('#share-user-search');
|
||||
if (!input) {
|
||||
throw new Error("Missing search element");
|
||||
}
|
||||
let currentlySelected: number|null = null;
|
||||
const autocomplete = new Autocomplete(input, {
|
||||
server: SEARCH_API_URL,
|
||||
liveServer: true,
|
||||
fullWidth: true,
|
||||
fixed: true,
|
||||
|
||||
onSelectItem(item: Object, instance: Autocomplete) {
|
||||
// @ts-expect-error
|
||||
currentlySelected = item.value;
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelector<HTMLButtonElement>('.js--adds-user')?.addEventListener('click', async (e) => {
|
||||
const formData = new FormData();
|
||||
formData.append('requested', currentlySelected?.toString() ?? '');
|
||||
formData.append('listid', getCurrentlySelectedList()?.toString() ?? '');
|
||||
|
||||
const response = await fetch(
|
||||
ADD_API_URL,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
dtApi.ajax.reload()
|
||||
})
|
||||
|
||||
const table = document.querySelector<HTMLTableElement>('#shared-users-table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
new DataTable(table, {
|
||||
dtApi = new DataTable(table, {
|
||||
columns: [
|
||||
{
|
||||
name: '',
|
||||
title: 'Icon',
|
||||
data: 'icon',
|
||||
className: 'avatar',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
width: 'auto',
|
||||
render(data, type) {
|
||||
if (type !== 'display') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return `<img src="${ data }" alt="Profile Picture" class="avatar-icon h-100 position-relative me-2 ratio-1 rounded-circle" />`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
data: 'name',
|
||||
title: 'Name'
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
data: 'id',
|
||||
render: actionColumnRender,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
type: 'html'
|
||||
}
|
||||
],
|
||||
ajax: {
|
||||
url: DT_API_URL,
|
||||
data(data) {
|
||||
// @ts-expect-error
|
||||
data.listid = getCurrentlySelectedList()
|
||||
}
|
||||
},
|
||||
order: [ [1, "desc"] ]
|
||||
});
|
||||
}
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace GamesShop\Entities;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use GamesShop\Entities\Account\User;
|
||||
|
@ -35,6 +36,7 @@ final class GamesList
|
|||
{
|
||||
$this->owner = $owner;
|
||||
$this->name = $name;
|
||||
$this->claimer = new ArrayCollection([$owner]);
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,10 +55,13 @@ final class GamesList
|
|||
return $this->name;
|
||||
}
|
||||
|
||||
public function getClaimer(): array
|
||||
public function getClaimer(): Collection
|
||||
{
|
||||
return $this->claimer;
|
||||
}
|
||||
|
||||
|
||||
public function addClaimer(User $claimer): void
|
||||
{
|
||||
$this->claimer[] = $claimer;
|
||||
}
|
||||
}
|
|
@ -119,7 +119,7 @@ final class GameImporter
|
|||
}
|
||||
}
|
||||
|
||||
if ($values['key'] === null || $values['name'] === null || $values['store'] === null) {
|
||||
if (empty($values['key']) || empty($values['name']) || empty($values['store'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,5 +12,6 @@ final class DataTablesAPIRoutes
|
|||
AccountsEndpoint::applyRoutes($group);
|
||||
|
||||
$group->get('/keys/provider', ProviderKeysEndpoint::class);
|
||||
$group->get('/list/users', SharedUsersEndpoint::class);
|
||||
}
|
||||
}
|
63
src/php/Routing/Api/DataTables/SharedUsersEndpoint.php
Normal file
63
src/php/Routing/Api/DataTables/SharedUsersEndpoint.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace GamesShop\Routing\Api\DataTables;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GamesShop\Entities\Account\User;
|
||||
use GamesShop\Entities\GamesList;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use League\Route\Http\Exception\BadRequestException;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class SharedUsersEndpoint
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler,
|
||||
private readonly EntityManager $entityManager,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* @throws UnauthorizedException
|
||||
* @throws ForbiddenException
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$body = $request->getQueryParams();
|
||||
if (!array_key_exists('listid', $body)) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
$list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]);
|
||||
$claimer = $list->getClaimer();
|
||||
|
||||
return new JsonResponse(
|
||||
[ 'data' => $claimer
|
||||
->filter(fn ($claimerUser) => $claimerUser !== $user)
|
||||
->map(
|
||||
function (User $user) {
|
||||
|
||||
return [
|
||||
'id' => $user->getId(),
|
||||
'name' => $user->getName(),
|
||||
'icon' => $user->getProfilePictureUrl()
|
||||
];
|
||||
}
|
||||
)->toArray()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
61
src/php/Routing/Api/Web/AddUserToList.php
Normal file
61
src/php/Routing/Api/Web/AddUserToList.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace GamesShop\Routing\Api\Web;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GamesShop\Entities\Account\User;
|
||||
use GamesShop\Entities\GamesList;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response;
|
||||
use League\Route\Http\Exception\BadRequestException;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class AddUserToList
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler,
|
||||
private readonly EntityManager $entityManager,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
if (!array_key_exists('listid', $body)) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
$list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]);
|
||||
if (!$list instanceof GamesList) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
|
||||
$requestedUser = $this->entityManager->getRepository(User::class)
|
||||
->find($body['requested']);
|
||||
|
||||
if ($list->getClaimer()->contains($requestedUser)) {
|
||||
return new Response();
|
||||
}
|
||||
$list->addClaimer($requestedUser);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new Response();
|
||||
}
|
||||
}
|
60
src/php/Routing/Api/Web/RemoveUserFromList.php
Normal file
60
src/php/Routing/Api/Web/RemoveUserFromList.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace GamesShop\Routing\Api\Web;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GamesShop\Entities\Account\User;
|
||||
use GamesShop\Entities\GamesList;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response;
|
||||
use League\Route\Http\Exception\BadRequestException;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class RemoveUserFromList
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler,
|
||||
private readonly EntityManager $entityManager,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
if (!array_key_exists('listid', $body)) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
$list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]);
|
||||
if (!$list instanceof GamesList) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
$requestedUser = $this->entityManager->getRepository(User::class)
|
||||
->find($body['requestedUser']);
|
||||
|
||||
if ($requestedUser === $user) {
|
||||
return new Response();
|
||||
}
|
||||
|
||||
$list->getClaimer()->removeElement($requestedUser);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new Response();
|
||||
}
|
||||
}
|
59
src/php/Routing/Api/Web/SearchForUsers.php
Normal file
59
src/php/Routing/Api/Web/SearchForUsers.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace GamesShop\Routing\Api\Web;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GamesShop\Entities\Account\User;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class SearchForUsers
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler,
|
||||
private readonly EntityManager $entityManager,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* @throws ForbiddenException
|
||||
* @throws UnauthorizedException
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$searchQuery = $request->getQueryParams()['query'] ?? '';
|
||||
|
||||
$repo = $this->entityManager->getRepository(User::class);
|
||||
|
||||
$criteria = Criteria::create();
|
||||
$criteria->where(Criteria::expr()->contains('name', $searchQuery));
|
||||
$criteria->setMaxResults(10);
|
||||
|
||||
$values = $repo->matching($criteria);
|
||||
return new JsonResponse(
|
||||
$values
|
||||
->filter(fn ($value) => $value !== $user)
|
||||
->map(function (User $user) {
|
||||
return [
|
||||
'value' => $user->getId(),
|
||||
'label' => $user->getName()
|
||||
];
|
||||
})
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,5 +14,9 @@ final class WebAPIRoutes
|
|||
$group->post('/keys/import/perform', ImportKeysRoute::class);
|
||||
|
||||
$group->post('/keys/list/create', CreateKeyListRoute::class);
|
||||
|
||||
$group->get('/share/search', SearchForUsers::class);
|
||||
$group->post('/share/add', AddUserToList::class);
|
||||
$group->post('/share/remove', RemoveUserFromList::class);
|
||||
}
|
||||
}
|
|
@ -20,20 +20,11 @@ use Psr\Http\Message\ServerRequestInterface;
|
|||
|
||||
final class Router
|
||||
{
|
||||
public function __construct(
|
||||
private ResourceRoute $resourceRoute
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function route(): ResponseInterface
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals(
|
||||
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
|
||||
);
|
||||
if ($this->resourceRoute->isValid($request->getUri())) {
|
||||
return $this->resourceRoute->getResponse($request->getUri());
|
||||
}
|
||||
|
||||
$router = new \League\Route\Router;
|
||||
$strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance());
|
||||
|
|
|
@ -96,8 +96,7 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
|||
|
||||
<button class="btn btn-primary js--do-import">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</button></div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="share-tab-pane" role="tabpanel">
|
||||
<h2>
|
||||
|
@ -107,16 +106,12 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
|||
<label for="share-user-search">Search for a user...</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="share-user-search" placeholder="">
|
||||
<button class="btn btn-primary js--search-shared-user">Search</button>
|
||||
<button class="btn btn-primary js--adds-user">+ Add</button>
|
||||
</div>
|
||||
|
||||
<h3>Users shared to</h3>
|
||||
<table id="shared-users-table" class="table table-striped w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="2.4rem"></th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
|
|
@ -89,6 +89,9 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts'],
|
||||
fallback: {
|
||||
'bootstrap5-autocomplete/types/autocomplete': 'bootstrap5-autocomplete'
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
index: JS_FOLDER + "/pages/index",
|
||||
|
|
Loading…
Reference in a new issue