implemented adding required tasks to task
This commit is contained in:
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.company;
|
||||
import static de.srsoftware.umbrella.company.Constants.CONFIG_DATABASE;
|
||||
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Constants.FULLTEXT;
|
||||
import static de.srsoftware.umbrella.core.Paths.LIST;
|
||||
import static de.srsoftware.umbrella.core.Paths.SEARCH;
|
||||
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||
|
||||
@@ -4,13 +4,13 @@ package de.srsoftware.umbrella.core.api;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Task;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface TaskService {
|
||||
HashMap<Long, Task> listCompanyTasks(long companyId) throws UmbrellaException;
|
||||
HashMap<Long, Task> listProjectTasks(long projectId) throws UmbrellaException;
|
||||
HashMap<Long, Task> load(Collection<Long> taskIds);
|
||||
Map<Long, Task> listCompanyTasks(long companyId) throws UmbrellaException;
|
||||
Map<Long, Task> listProjectTasks(long projectId) throws UmbrellaException;
|
||||
Map<Long, Task> load(Collection<Long> taskIds);
|
||||
Collection<Task> loadMembers(Collection<Task> tasks);
|
||||
|
||||
default Task loadMembers(Task task){
|
||||
|
||||
@@ -164,6 +164,7 @@ public class Task implements Mappable {
|
||||
case NAME: name = json.getString(key); break;
|
||||
case NO_INDEX: noIndex = json.getBoolean(NO_INDEX); break;
|
||||
case PARENT_TASK_ID: parentTaskId = json.getLong(PARENT_TASK_ID); break;
|
||||
case REQUIRED_TASKS_IDS: requiredTasksIds.addAll(json.getJSONArray(REQUIRED_TASKS_IDS).toList().stream().map(entry -> Long.parseLong(entry.toString())).toList()); break;
|
||||
case SHOW_CLOSED: showClosed = json.getBoolean(SHOW_CLOSED); break;
|
||||
case START_DATE: start = json.isNull(START_DATE) || json.getString(START_DATE).isBlank() ? null : LocalDate.parse(json.getString(START_DATE)); break;
|
||||
case STATUS: status = json.getInt(key); break;
|
||||
|
||||
@@ -7,7 +7,6 @@ import de.srsoftware.umbrella.core.model.Document;
|
||||
import de.srsoftware.umbrella.core.model.Template;
|
||||
import de.srsoftware.umbrella.core.model.Type;
|
||||
import de.srsoftware.umbrella.documents.model.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public interface DocumentDb {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</tr>
|
||||
{/each}
|
||||
<tr>
|
||||
<td>{t('add_member')}</td>
|
||||
<td>{t('add_object',{object:t('member')})}</td>
|
||||
<td>
|
||||
<Autocomplete {getCandidates} {onSelect} />
|
||||
</td>
|
||||
|
||||
110
frontend/src/routes/task/RequiredTasks.svelte
Normal file
110
frontend/src/routes/task/RequiredTasks.svelte
Normal file
@@ -0,0 +1,110 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
|
||||
import { api } from '../../urls.svelte';
|
||||
import { t } from '../../translations.svelte';
|
||||
|
||||
let { task = null } = $props();
|
||||
|
||||
let candidates = $state({});
|
||||
let error = $state(null);
|
||||
let key = $state(null);
|
||||
let requiredTasks = $state({});
|
||||
let router = useTinyRouter();
|
||||
let sortedTasks = $derived(Object.values(candidates).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
let timer = null;
|
||||
|
||||
async function add(new_task_id){
|
||||
let url = api(`task/${new_task_id}`);
|
||||
let resp = await fetch(url,{ credentials : 'include' });
|
||||
if (resp.ok){
|
||||
error = null;
|
||||
let newTask = await resp.json();
|
||||
if (newTask.project_id != task.project_id){
|
||||
alert('prject mismatch!');
|
||||
return;
|
||||
}
|
||||
task.required_tasks_ids.push(new_task_id);
|
||||
requiredTasks[new_task_id] = newTask;
|
||||
url = api(`task/${task.id}`);
|
||||
resp = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'PATCH',
|
||||
body : JSON.stringify({required_tasks_ids:task.required_tasks_ids})
|
||||
});
|
||||
delete candidates[new_task_id];
|
||||
}
|
||||
if (!resp.ok) {
|
||||
error = await resp.text();
|
||||
}
|
||||
}
|
||||
|
||||
async function doSearch(){
|
||||
if (!key.trim()) {
|
||||
candidates = {};
|
||||
return;
|
||||
}
|
||||
const data = { key : key, project_id : task?.project_id };
|
||||
const url = api('task/search');
|
||||
const res = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
error = null;
|
||||
candidates = await res.json();
|
||||
for (var taskId of Object.keys(requiredTasks)) delete candidates[taskId];
|
||||
delete candidates[task.id];
|
||||
} else {
|
||||
error = await res.text();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTasks(){
|
||||
if (!task || !task.required_tasks_ids || !task.required_tasks_ids.length) return;
|
||||
const url = api('task/list');
|
||||
const res = await fetch(url,{
|
||||
credentials : 'include',
|
||||
method : 'POST',
|
||||
body : JSON.stringify({ids:task.required_tasks_ids})
|
||||
});
|
||||
if (res.ok){
|
||||
error = null;
|
||||
requiredTasks = await res.json();
|
||||
} else {
|
||||
error = await res.text();
|
||||
}
|
||||
}
|
||||
|
||||
function oninput(){
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(doSearch,1000);
|
||||
}
|
||||
|
||||
function openTask(e){
|
||||
e.preventDefault();
|
||||
let href = e.target.getAttribute('href');
|
||||
if (href) router.navigate(href);
|
||||
return false;
|
||||
}
|
||||
|
||||
onMount(loadTasks);
|
||||
</script>
|
||||
|
||||
{#if requiredTasks}
|
||||
<ul class="required task list">
|
||||
{#each Object.values(requiredTasks) as task}
|
||||
<li class="task" >
|
||||
<a href="/task/{task.id}/view" onclick={openTask} >{task.name}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{t('add_object',{object:t('task')})}: <input type="text" bind:value={key} {oninput} />
|
||||
<div class="candidate task list">
|
||||
{#each sortedTasks as task}
|
||||
<div class="task" onclick={e => add(task.id)}>{task.name}</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -12,6 +12,7 @@
|
||||
import PermissionEditor from '../../Components/PermissionEditor.svelte';
|
||||
import Notes from '../notes/RelatedNotes.svelte';
|
||||
import StateSelector from '../../Components/StateSelector.svelte';
|
||||
import RequiredTasks from './RequiredTasks.svelte';
|
||||
import TagList from '../tags/TagList.svelte';
|
||||
import TaskList from './TaskList.svelte';
|
||||
|
||||
@@ -301,6 +302,14 @@
|
||||
<input type="number" bind:value={task.estimated_time} onchange={() => update({estimated_time:task.estimated_time})} /> h
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
{t('depends_on')}
|
||||
</th>
|
||||
<td>
|
||||
<RequiredTasks {task} />
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package de.srsoftware.umbrella.notes;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Note;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Note;
|
||||
import de.srsoftware.umbrella.core.model.Project;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
|
||||
@@ -9,7 +9,6 @@ import static de.srsoftware.umbrella.core.model.Status.COMPLETE;
|
||||
import static de.srsoftware.umbrella.core.model.Status.OPEN;
|
||||
import static de.srsoftware.umbrella.project.Constants.*;
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
|
||||
@@ -4,11 +4,10 @@ package de.srsoftware.umbrella.task;
|
||||
public class Constants {
|
||||
private Constants(){}
|
||||
|
||||
|
||||
|
||||
public static final String CONFIG_DATABASE = "umbrella.modules.task.database";
|
||||
public static final String CHILDREN = "children";
|
||||
public static final String ESTIMATED_TIMES = "estimated_times";
|
||||
public static final String IDS = "ids";
|
||||
public static final String REQUIRED_TASK_ID = "required_task_id";
|
||||
public static final String TABLE_TASK_DEPENDENCIES = "task_dependencies";
|
||||
public static final String TABLE_TASKS = "tasks";
|
||||
|
||||
@@ -166,14 +166,7 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void init(){
|
||||
var version = createTables();
|
||||
LOG.log(INFO,"Updated task db to version {0}",version);
|
||||
}
|
||||
|
||||
|
||||
public HashMap<Long, Task> listTasks(Collection<Long> projectIds) throws UmbrellaException {
|
||||
public Map<Long, Task> listTasks(Collection<Long> projectIds) throws UmbrellaException {
|
||||
try {
|
||||
var tasks = new HashMap<Long,Task>();
|
||||
var rs = select(ALL).from(TABLE_TASKS).where(PROJECT_ID, in(projectIds.toArray())).exec(db);
|
||||
@@ -182,13 +175,13 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
tasks.put(task.id(),task);
|
||||
}
|
||||
rs.close();
|
||||
return tasks;
|
||||
return loadDependencies(tasks);
|
||||
} catch (SQLException e) {
|
||||
throw new UmbrellaException("Failed to load tasks for project ids");
|
||||
throw databaseException("Failed to load tasks for project ids");
|
||||
}
|
||||
}
|
||||
|
||||
public HashMap<Long, Task> listRootTasks(Long projectId, UmbrellaUser user, boolean showClosed) {
|
||||
public Map<Long, Task> listRootTasks(Long projectId, UmbrellaUser user, boolean showClosed) {
|
||||
try {
|
||||
var tasks = new HashMap<Long,Task>();
|
||||
var query = select(ALL).from(TABLE_TASKS).leftJoin(ID,TABLE_TASKS_USERS,TASK_ID)
|
||||
@@ -202,14 +195,14 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
tasks.put(task.id(),task);
|
||||
}
|
||||
rs.close();
|
||||
return tasks;
|
||||
return loadDependencies(tasks);
|
||||
} catch (SQLException e){
|
||||
LOG.log(WARNING,"Failed to load tasks for project (pid: {0}, user_id: {1}",projectId,user.id(),e);
|
||||
throw new UmbrellaException("Failed to load tasks for project id");
|
||||
throw databaseException("Failed to load tasks for project id");
|
||||
}
|
||||
}
|
||||
|
||||
public HashMap<Long, Task> listChildrenOf(Long parentTaskId, UmbrellaUser user, boolean showClosed) {
|
||||
public Map<Long, Task> listChildrenOf(Long parentTaskId, UmbrellaUser user, boolean showClosed) {
|
||||
try {
|
||||
var tasks = new HashMap<Long,Task>();
|
||||
var query = select(ALL).from(TABLE_TASKS).leftJoin(ID,TABLE_TASKS_USERS,TASK_ID)
|
||||
@@ -222,15 +215,15 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
tasks.put(task.id(),task);
|
||||
}
|
||||
rs.close();
|
||||
return tasks;
|
||||
return loadDependencies(tasks);
|
||||
} catch (SQLException e){
|
||||
LOG.log(WARNING,"Failed to load child tasks (parentTaskId: {0}, user_id: {1}",parentTaskId,user.id(),e);
|
||||
throw new UmbrellaException("Failed to load tasks for project id");
|
||||
throw databaseException("Failed to load tasks for project id");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Long, Task> listProjectTasks(Long projectId, Long parentTaskId, boolean noIndex) throws UmbrellaException {
|
||||
public Map<Long, Task> listProjectTasks(Long projectId, Long parentTaskId, boolean noIndex) throws UmbrellaException {
|
||||
try {
|
||||
var query = select(ALL).from(TABLE_TASKS).where(PROJECT_ID,equal(projectId));
|
||||
if (parentTaskId != 0) query.where(PARENT_TASK_ID,equal(parentTaskId));
|
||||
@@ -242,9 +235,9 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
tasks.put(task.id(),task);
|
||||
}
|
||||
rs.close();
|
||||
return tasks;
|
||||
return loadDependencies(tasks);
|
||||
} catch (SQLException e){
|
||||
throw new UmbrellaException("Failed to load tasks for project {0}",projectId);
|
||||
throw databaseException("Failed to load tasks for project {0}",projectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,18 +258,43 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
|
||||
@Override
|
||||
public Task load(long taskId) throws UmbrellaException {
|
||||
var map = load(List.of(taskId));
|
||||
var task = map.get(taskId);
|
||||
if (task == null) throw UmbrellaException.notFound("No task found for id {0}",taskId);
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, Task> load(Collection<Long> taskIds) throws UmbrellaException {
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_TASKS).where(ID, equal(taskId)).exec(db);
|
||||
Task result = null;
|
||||
if (rs.next()) result = Task.of(rs);
|
||||
var rs = select(ALL).from(TABLE_TASKS).where(ID, in(taskIds.toArray())).exec(db);
|
||||
var map = new HashMap<Long, Task>();
|
||||
while (rs.next()) {
|
||||
var task = Task.of(rs);
|
||||
map.put(task.id(),task);
|
||||
}
|
||||
rs.close();
|
||||
if (result == null) throw UmbrellaException.notFound("No task found for id {0}",taskId);
|
||||
return result;
|
||||
return loadDependencies(map);
|
||||
} catch (SQLException e) {
|
||||
throw new UmbrellaException("Failed to load task from database");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, Task> loadDependencies(Map<Long, Task> tasks) {
|
||||
try {
|
||||
var rs = select(ALL).from(TABLE_TASK_DEPENDENCIES).where(TASK_ID,in(tasks.keySet().toArray())).exec(db);
|
||||
while (rs.next()){
|
||||
var taskId = rs.getLong(TASK_ID);
|
||||
var requiredTaskId = rs.getLong(REQUIRED_TASK_ID);
|
||||
tasks.get(taskId).requiredTasksIds().add(requiredTaskId);
|
||||
}
|
||||
rs.close();
|
||||
return tasks;
|
||||
} catch (SQLException e) {
|
||||
throw databaseException("Failed to load task dependencies");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task save(Task task) {
|
||||
try {
|
||||
@@ -297,6 +315,12 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
query.execute(db).close();
|
||||
task.clean(MEMBERS);
|
||||
}
|
||||
if (task.isDirty(REQUIRED_TASKS_IDS)) {
|
||||
var query = replaceInto(TABLE_TASK_DEPENDENCIES,TASK_ID,REQUIRED_TASK_ID);
|
||||
for (var reqId : task.requiredTasksIds()) query.values(task.id(),reqId);
|
||||
query.execute(db).close();
|
||||
task.clean(REQUIRED_TASKS_IDS);
|
||||
}
|
||||
if (task.isDirty()) {
|
||||
update(TABLE_TASKS).set(PROJECT_ID,PARENT_TASK_ID,NAME,DESCRIPTION,STATUS,EST_TIME,START_DATE,DUE_DATE,SHOW_CLOSED,NO_INDEX)
|
||||
.where(ID,equal(task.id())).prepare(db)
|
||||
|
||||
@@ -7,7 +7,6 @@ import de.srsoftware.umbrella.core.model.Permission;
|
||||
import de.srsoftware.umbrella.core.model.Task;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -15,16 +14,17 @@ public interface TaskDb {
|
||||
|
||||
void delete(Task task) throws UmbrellaException;
|
||||
void dropMember(long projectId, long userId);
|
||||
HashMap<Long, Task> find(long userId, List<String> keys, boolean fulltext);
|
||||
Map<Long, Task> find(long userId, List<String> keys, boolean fulltext);
|
||||
Map<Long, Permission> getMembers(Task task);
|
||||
HashMap<Long, Task> listChildrenOf(Long parentTaskId, UmbrellaUser user, boolean showClosed);
|
||||
HashMap<Long, Task> listProjectTasks(Long projectId, Long parentTaskId, boolean noIndex) throws UmbrellaException;
|
||||
HashMap<Long, Task> listRootTasks(Long projectId, UmbrellaUser user, boolean showClosed);
|
||||
HashMap<Long, Task> listTasks(Collection<Long> projectIds) throws UmbrellaException;
|
||||
Map<Long, Task> listChildrenOf(Long parentTaskId, UmbrellaUser user, boolean showClosed);
|
||||
Map<Long, Task> listProjectTasks(Long projectId, Long parentTaskId, boolean noIndex) throws UmbrellaException;
|
||||
Map<Long, Task> listRootTasks(Long projectId, UmbrellaUser user, boolean showClosed);
|
||||
Map<Long, Task> listTasks(Collection<Long> projectIds) throws UmbrellaException;
|
||||
|
||||
List<Task> listUserTasks(long userId, Long limit, long offset, boolean showClosed);
|
||||
|
||||
Task load(long taskId) throws UmbrellaException;
|
||||
Map<Long, Task> load(Collection<Long> taskIds) throws UmbrellaException;
|
||||
|
||||
Task save(Task task);
|
||||
|
||||
|
||||
@@ -206,13 +206,13 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Long, Task> listCompanyTasks(long companyId) throws UmbrellaException {
|
||||
public Map<Long, Task> listCompanyTasks(long companyId) throws UmbrellaException {
|
||||
var projectList = projectService().listCompanyProjects(companyId,false);
|
||||
return taskDb.listTasks(projectList.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Long, Task> listProjectTasks(long projectId) throws UmbrellaException {
|
||||
public Map<Long, Task> listProjectTasks(long projectId) throws UmbrellaException {
|
||||
return taskDb.listTasks(List.of(projectId));
|
||||
}
|
||||
|
||||
@@ -226,13 +226,8 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Long, Task> load(Collection<Long> taskIds) {
|
||||
try {
|
||||
var map = taskIds.stream().map(this::loadTaskOrNull).filter(Objects::nonNull).collect(Collectors.toMap(Task::id, t -> t));
|
||||
return new HashMap<>(map);
|
||||
} catch (Exception e){
|
||||
throw new UmbrellaException(e.getMessage());
|
||||
}
|
||||
public Map<Long, Task> load(Collection<Long> taskIds) {
|
||||
return taskDb.load(taskIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -359,9 +354,11 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY);
|
||||
var projectId = json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number pid ? pid.longValue() : null;
|
||||
var keys = Arrays.asList(key.split(" "));
|
||||
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
|
||||
var tasks = taskDb.find(user.id(),keys,fulltext);
|
||||
if (projectId != null) tasks = tasks.values().stream().filter(task -> task.projectId() == projectId).collect(Collectors.toMap(Task::id, t -> t));
|
||||
return sendContent(ex,mapValues(tasks));
|
||||
}
|
||||
|
||||
@@ -379,6 +376,8 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
return sendContent(ex,mapValues(projectTasks));
|
||||
}
|
||||
if (isSet(parentTaskId)) return sendContent(ex,mapValues(taskDb.listChildrenOf(parentTaskId,user,showClosed)));
|
||||
var taskIds = json.has(IDS) && json.get(IDS) instanceof JSONArray ids ? ids.toList().stream().map(Object::toString).map(Long::parseLong).toList() : null;
|
||||
if (isSet(taskIds)) return sendContent(ex,mapValues(taskDb.load(taskIds)));
|
||||
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package de.srsoftware.umbrella.time;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Time;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"delete_object": "{object} löschen",
|
||||
"DELETE_USERS": "Nutzer löschen",
|
||||
"delivery_date": "Lieferdatum",
|
||||
"depends_on": "hängt ab von",
|
||||
"description": "Beschreibung",
|
||||
"detail": "Details",
|
||||
"display_closed_tasks": "abgeschlossene Aufgaben anzeigen",
|
||||
@@ -117,6 +118,7 @@
|
||||
"long_click_to_edit": "lang klicken zum Bearbeiten",
|
||||
|
||||
"MANAGE_LOGIN_SERVICES": "Login-Services verwalten",
|
||||
"member": "Mitarbeiter",
|
||||
"members": "Mitarbeiter",
|
||||
"message": "Nachricht",
|
||||
"messages": "Benachrichtigungen",
|
||||
|
||||
Reference in New Issue
Block a user