mirror of
https://github.com/nextcloud/android.git
synced 2024-11-21 20:55:31 +03:00
Support for viewing tags
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
parent
ffac056474
commit
f196bfd874
14 changed files with 1268 additions and 39 deletions
1137
app/schemas/com.nextcloud.client.database.NextcloudDatabase/69.json
Normal file
1137
app/schemas/com.nextcloud.client.database.NextcloudDatabase/69.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -48,20 +48,23 @@ class OCFileListFragmentStaticServerIT : AbstractIT() {
|
||||||
fun showFiles() {
|
fun showFiles() {
|
||||||
val sut = testActivityRule.launchActivity(null)
|
val sut = testActivityRule.launchActivity(null)
|
||||||
|
|
||||||
val textFile = OCFile("/1.png")
|
OCFile("/1.png").apply {
|
||||||
textFile.mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
textFile.fileLength = 1024000
|
fileLength = 1024000
|
||||||
textFile.modificationTimestamp = 1188206955000
|
modificationTimestamp = 1188206955000
|
||||||
textFile.parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
|
parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
|
||||||
sut.storageManager.saveFile(textFile)
|
sut.storageManager.saveFile(this)
|
||||||
|
}
|
||||||
|
|
||||||
val imageFile = OCFile("/image.png")
|
OCFile("/image.png").apply {
|
||||||
imageFile.mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
imageFile.isPreviewAvailable = false
|
isPreviewAvailable = false
|
||||||
imageFile.fileLength = 3072000
|
fileLength = 3072000
|
||||||
imageFile.modificationTimestamp = 746443755000
|
modificationTimestamp = 746443755000
|
||||||
imageFile.parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
|
parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
|
||||||
sut.storageManager.saveFile(imageFile)
|
tags = listOf("Top secret")
|
||||||
|
sut.storageManager.saveFile(this)
|
||||||
|
}
|
||||||
|
|
||||||
OCFile("/video.mp4").apply {
|
OCFile("/video.mp4").apply {
|
||||||
mimeType = "video/mp4"
|
mimeType = "video/mp4"
|
||||||
|
@ -69,6 +72,7 @@ class OCFileListFragmentStaticServerIT : AbstractIT() {
|
||||||
fileLength = 12092000
|
fileLength = 12092000
|
||||||
modificationTimestamp = 746143952000
|
modificationTimestamp = 746143952000
|
||||||
parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
|
parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
|
||||||
|
tags = listOf("Confidential", "+5")
|
||||||
sut.storageManager.saveFile(this)
|
sut.storageManager.saveFile(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -117,5 +117,7 @@ data class FileEntity(
|
||||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TIMEOUT)
|
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TIMEOUT)
|
||||||
val lockTimeout: Int?,
|
val lockTimeout: Int?,
|
||||||
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TOKEN)
|
@ColumnInfo(name = ProviderTableMeta.FILE_LOCK_TOKEN)
|
||||||
val lockToken: String?
|
val lockToken: String?,
|
||||||
|
@ColumnInfo(name = ProviderTableMeta.FILE_TAGS)
|
||||||
|
val tags: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -440,6 +440,7 @@ public class FileDataStorageManager {
|
||||||
*/
|
*/
|
||||||
private ContentValues createContentValuesBase(OCFile fileOrFolder) {
|
private ContentValues createContentValuesBase(OCFile fileOrFolder) {
|
||||||
final ContentValues cv = new ContentValues();
|
final ContentValues cv = new ContentValues();
|
||||||
|
final Gson gson = new Gson();
|
||||||
cv.put(ProviderTableMeta.FILE_MODIFIED, fileOrFolder.getModificationTimestamp());
|
cv.put(ProviderTableMeta.FILE_MODIFIED, fileOrFolder.getModificationTimestamp());
|
||||||
cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, fileOrFolder.getModificationTimestampAtLastSyncForData());
|
cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, fileOrFolder.getModificationTimestampAtLastSyncForData());
|
||||||
cv.put(ProviderTableMeta.FILE_PARENT, fileOrFolder.getParentId());
|
cv.put(ProviderTableMeta.FILE_PARENT, fileOrFolder.getParentId());
|
||||||
|
@ -464,7 +465,8 @@ public class FileDataStorageManager {
|
||||||
cv.put(ProviderTableMeta.FILE_OWNER_ID, fileOrFolder.getOwnerId());
|
cv.put(ProviderTableMeta.FILE_OWNER_ID, fileOrFolder.getOwnerId());
|
||||||
cv.put(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME, fileOrFolder.getOwnerDisplayName());
|
cv.put(ProviderTableMeta.FILE_OWNER_DISPLAY_NAME, fileOrFolder.getOwnerDisplayName());
|
||||||
cv.put(ProviderTableMeta.FILE_NOTE, fileOrFolder.getNote());
|
cv.put(ProviderTableMeta.FILE_NOTE, fileOrFolder.getNote());
|
||||||
cv.put(ProviderTableMeta.FILE_SHAREES, new Gson().toJson(fileOrFolder.getSharees()));
|
cv.put(ProviderTableMeta.FILE_SHAREES, gson.toJson(fileOrFolder.getSharees()));
|
||||||
|
cv.put(ProviderTableMeta.FILE_TAGS, gson.toJson(fileOrFolder.getTags()));
|
||||||
cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, fileOrFolder.getRichWorkspace());
|
cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, fileOrFolder.getRichWorkspace());
|
||||||
return cv;
|
return cv;
|
||||||
}
|
}
|
||||||
|
@ -952,6 +954,20 @@ public class FileDataStorageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String tags = fileEntity.getTags();
|
||||||
|
if (tags == null || tags.isEmpty() ||
|
||||||
|
JSON_NULL_STRING.equals(tags) || JSON_EMPTY_ARRAY.equals(tags)) {
|
||||||
|
ocFile.setTags(new ArrayList<>());
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String[] tagsArray = gson.fromJson(tags, String[].class);
|
||||||
|
ocFile.setTags(new ArrayList<>(Arrays.asList(tagsArray)));
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
// ignore saved value due to api change
|
||||||
|
ocFile.setTags(new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String metadataSize = fileEntity.getMetadataSize();
|
String metadataSize = fileEntity.getMetadataSize();
|
||||||
// Surprisingly JSON deserialization causes significant overhead.
|
// Surprisingly JSON deserialization causes significant overhead.
|
||||||
// Avoid it in common, trivial cases (null/empty).
|
// Avoid it in common, trivial cases (null/empty).
|
||||||
|
|
|
@ -114,6 +114,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
|
||||||
private String lockToken;
|
private String lockToken;
|
||||||
@Nullable
|
@Nullable
|
||||||
private ImageDimension imageDimension;
|
private ImageDimension imageDimension;
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URI to the local path of the file contents, if stored in the device; cached after first call to {@link
|
* URI to the local path of the file contents, if stored in the device; cached after first call to {@link
|
||||||
|
@ -966,4 +967,12 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
|
||||||
public ImageDimension getImageDimension() {
|
public ImageDimension getImageDimension() {
|
||||||
return imageDimension;
|
return imageDimension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getTags() {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTags(List<String> tags) {
|
||||||
|
this.tags = tags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class ProviderMeta {
|
public class ProviderMeta {
|
||||||
public static final String DB_NAME = "filelist";
|
public static final String DB_NAME = "filelist";
|
||||||
public static final int DB_VERSION = 68;
|
public static final int DB_VERSION = 69;
|
||||||
|
|
||||||
private ProviderMeta() {
|
private ProviderMeta() {
|
||||||
// No instance
|
// No instance
|
||||||
|
@ -125,6 +125,7 @@ public class ProviderMeta {
|
||||||
public static final String FILE_LOCK_TIMESTAMP = "lock_timestamp";
|
public static final String FILE_LOCK_TIMESTAMP = "lock_timestamp";
|
||||||
public static final String FILE_LOCK_TIMEOUT = "lock_timeout";
|
public static final String FILE_LOCK_TIMEOUT = "lock_timeout";
|
||||||
public static final String FILE_LOCK_TOKEN = "lock_token";
|
public static final String FILE_LOCK_TOKEN = "lock_token";
|
||||||
|
public static final String FILE_TAGS = "tags";
|
||||||
|
|
||||||
public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
|
public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
|
||||||
_ID,
|
_ID,
|
||||||
|
@ -171,7 +172,8 @@ public class ProviderMeta {
|
||||||
FILE_LOCK_TIMESTAMP,
|
FILE_LOCK_TIMESTAMP,
|
||||||
FILE_LOCK_TIMEOUT,
|
FILE_LOCK_TIMEOUT,
|
||||||
FILE_LOCK_TOKEN,
|
FILE_LOCK_TOKEN,
|
||||||
FILE_METADATA_SIZE));
|
FILE_METADATA_SIZE,
|
||||||
|
FILE_TAGS));
|
||||||
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
|
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
|
||||||
|
|
||||||
// Columns of ocshares table
|
// Columns of ocshares table
|
||||||
|
|
|
@ -32,4 +32,6 @@ internal interface ListItemViewHolder : ListGridItemViewHolder {
|
||||||
val lastModification: TextView
|
val lastModification: TextView
|
||||||
val overflowMenu: ImageView
|
val overflowMenu: ImageView
|
||||||
val sharedAvatars: AvatarGroupLayout
|
val sharedAvatars: AvatarGroupLayout
|
||||||
|
val tag: TextView
|
||||||
|
val tagMore: TextView
|
||||||
}
|
}
|
||||||
|
|
|
@ -415,6 +415,24 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
||||||
holder.getSharedAvatars().removeAllViews();
|
holder.getSharedAvatars().removeAllViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tags
|
||||||
|
if (file.getTags().isEmpty()) {
|
||||||
|
holder.getTag().setVisibility(View.GONE);
|
||||||
|
holder.getTagMore().setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
holder.getTag().setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
holder.getTag().setText(file.getTags().get(0));
|
||||||
|
|
||||||
|
if (file.getTags().size() > 1) {
|
||||||
|
holder.getTagMore().setVisibility(View.VISIBLE);
|
||||||
|
holder.getTagMore().setText(String.format(activity.getString(R.string.tags_more),
|
||||||
|
(file.getTags().size() - 1)));
|
||||||
|
} else {
|
||||||
|
holder.getTagMore().setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// npe fix: looks like file without local storage path somehow get here
|
// npe fix: looks like file without local storage path somehow get here
|
||||||
final String storagePath = file.getStoragePath();
|
final String storagePath = file.getStoragePath();
|
||||||
if (onlyOnDevice && storagePath != null) {
|
if (onlyOnDevice && storagePath != null) {
|
||||||
|
|
|
@ -48,6 +48,10 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
|
||||||
get() = binding.Filename
|
get() = binding.Filename
|
||||||
override val thumbnail: ImageView
|
override val thumbnail: ImageView
|
||||||
get() = binding.thumbnailLayout.thumbnail
|
get() = binding.thumbnailLayout.thumbnail
|
||||||
|
override val tag: TextView
|
||||||
|
get() = binding.tag
|
||||||
|
override val tagMore: TextView
|
||||||
|
get() = binding.tagMore
|
||||||
|
|
||||||
override fun showVideoOverlay() {
|
override fun showVideoOverlay() {
|
||||||
binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE
|
binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE
|
||||||
|
|
|
@ -34,6 +34,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||||
import com.owncloud.android.datamodel.OCFile;
|
import com.owncloud.android.datamodel.OCFile;
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
import com.owncloud.android.lib.resources.files.model.RemoteFile;
|
import com.owncloud.android.lib.resources.files.model.RemoteFile;
|
||||||
|
import com.owncloud.android.lib.resources.shares.ShareeUser;
|
||||||
import com.owncloud.android.ui.helpers.FileOperationsHelper;
|
import com.owncloud.android.ui.helpers.FileOperationsHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -233,7 +234,7 @@ public final class FileStorageUtils {
|
||||||
file.setOwnerId(remote.getOwnerId());
|
file.setOwnerId(remote.getOwnerId());
|
||||||
file.setOwnerDisplayName(remote.getOwnerDisplayName());
|
file.setOwnerDisplayName(remote.getOwnerDisplayName());
|
||||||
file.setNote(remote.getNote());
|
file.setNote(remote.getNote());
|
||||||
file.setSharees(new ArrayList<>(Arrays.asList(remote.getSharees())));
|
file.setSharees(new ArrayList<ShareeUser>(Arrays.asList(remote.getSharees())));
|
||||||
file.setRichWorkspace(remote.getRichWorkspace());
|
file.setRichWorkspace(remote.getRichWorkspace());
|
||||||
file.setLocked(remote.isLocked());
|
file.setLocked(remote.isLocked());
|
||||||
file.setLockType(remote.getLockType());
|
file.setLockType(remote.getLockType());
|
||||||
|
@ -243,6 +244,7 @@ public final class FileStorageUtils {
|
||||||
file.setLockTimestamp(remote.getLockTimestamp());
|
file.setLockTimestamp(remote.getLockTimestamp());
|
||||||
file.setLockTimeout(remote.getLockTimeout());
|
file.setLockTimeout(remote.getLockTimeout());
|
||||||
file.setLockToken(remote.getLockToken());
|
file.setLockToken(remote.getLockToken());
|
||||||
|
file.setTags(new ArrayList<String>(Arrays.asList(remote.getTags())));
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,33 +95,65 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/file_size"
|
android:id="@+id/tag"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/placeholder_fileSize"
|
android:layout_marginEnd="@dimen/standard_eighth_margin"
|
||||||
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
||||||
android:textSize="@dimen/two_line_secondary_text_size" />
|
android:textSize="@dimen/two_line_secondary_text_size"
|
||||||
|
app:chipStartPadding="@dimen/standard_eighth_margin"
|
||||||
|
app:chipEndPadding="@dimen/standard_eighth_margin"
|
||||||
|
android:checkable="false"
|
||||||
|
android:ellipsize="middle" />
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/file_separator"
|
android:id="@+id/tag_more"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="end"
|
android:layout_marginStart="@dimen/standard_eighth_margin"
|
||||||
android:paddingStart="@dimen/zero"
|
android:layout_marginEnd="@dimen/standard_eighth_margin"
|
||||||
android:paddingEnd="@dimen/standard_quarter_padding"
|
|
||||||
android:text="@string/info_separator"
|
|
||||||
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
||||||
android:textSize="@dimen/two_line_secondary_text_size" />
|
android:textSize="@dimen/two_line_secondary_text_size"
|
||||||
|
app:chipStartPadding="@dimen/standard_eighth_margin"
|
||||||
|
app:chipEndPadding="@dimen/standard_eighth_margin"
|
||||||
|
app:chipBackgroundColor="@color/bg_default"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/last_mod"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:gravity="end"
|
android:orientation="horizontal"
|
||||||
android:text="@string/placeholder_media_time"
|
android:layout_weight="1">
|
||||||
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
|
||||||
android:textSize="@dimen/two_line_secondary_text_size" />
|
<TextView
|
||||||
|
android:id="@+id/file_size"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/placeholder_fileSize"
|
||||||
|
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
||||||
|
android:textSize="@dimen/two_line_secondary_text_size" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/file_separator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:paddingStart="@dimen/zero"
|
||||||
|
android:paddingEnd="@dimen/standard_quarter_padding"
|
||||||
|
android:text="@string/info_separator"
|
||||||
|
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
||||||
|
android:textSize="@dimen/two_line_secondary_text_size" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/last_mod"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/placeholder_media_time"
|
||||||
|
android:textColor="@color/list_item_lastmod_and_filesize_text"
|
||||||
|
android:textSize="@dimen/two_line_secondary_text_size" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -161,9 +193,9 @@
|
||||||
<com.owncloud.android.ui.AvatarGroupLayout
|
<com.owncloud.android.ui.AvatarGroupLayout
|
||||||
android:id="@+id/sharedAvatars"
|
android:id="@+id/sharedAvatars"
|
||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:layout_height="@dimen/file_icon_size"
|
android:layout_height="@dimen/file_icon_size"
|
||||||
android:contentDescription="@string/shared_avatar_desc"
|
android:contentDescription="@string/shared_avatar_desc"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:visibility="visible" />
|
android:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Beta indicator -->
|
<!-- Beta indicator -->
|
||||||
<bool name="is_beta">false</bool>
|
<bool name="is_beta">true</bool>
|
||||||
<bool name="dev_version_direct_download_enabled">false</bool>
|
<bool name="dev_version_direct_download_enabled">false</bool>
|
||||||
|
|
||||||
<!-- App name and other strings-->
|
<!-- App name and other strings-->
|
||||||
|
|
|
@ -1077,4 +1077,5 @@
|
||||||
<string name="document_scan_export_dialog_images">Multiple images</string>
|
<string name="document_scan_export_dialog_images">Multiple images</string>
|
||||||
<string name="download_cannot_create_file">Cannot create local file</string>
|
<string name="download_cannot_create_file">Cannot create local file</string>
|
||||||
<string name="download_download_invalid_local_file_name">Invalid filename for local file</string>
|
<string name="download_download_invalid_local_file_name">Invalid filename for local file</string>
|
||||||
|
<string name="tags_more">+%1$d</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -8,7 +8,7 @@ buildscript {
|
||||||
daggerVersion = "2.45"
|
daggerVersion = "2.45"
|
||||||
markwonVersion = "4.6.2"
|
markwonVersion = "4.6.2"
|
||||||
prismVersion = "2.0.0"
|
prismVersion = "2.0.0"
|
||||||
androidLibraryVersion = "master-SNAPSHOT"
|
androidLibraryVersion = "tags-SNAPSHOT"
|
||||||
mockitoVersion = "4.11.0"
|
mockitoVersion = "4.11.0"
|
||||||
mockitoKotlinVersion = "4.1.0"
|
mockitoKotlinVersion = "4.1.0"
|
||||||
mockkVersion = "1.13.3"
|
mockkVersion = "1.13.3"
|
||||||
|
|
Loading…
Reference in a new issue