Changes for JS actions in the import/export feature (#7892)
This commit is contained in:
parent
f8442622b3
commit
883b0215a6
|
|
@ -18,19 +18,21 @@ import java.util.Set;
|
||||||
public class ApplicationJson {
|
public class ApplicationJson {
|
||||||
|
|
||||||
Application exportedApplication;
|
Application exportedApplication;
|
||||||
|
|
||||||
List<Datasource> datasourceList;
|
List<Datasource> datasourceList;
|
||||||
|
|
||||||
List<NewPage> pageList;
|
List<NewPage> pageList;
|
||||||
|
|
||||||
String publishedDefaultPageName;
|
String publishedDefaultPageName;
|
||||||
|
|
||||||
String unpublishedDefaultPageName;
|
String unpublishedDefaultPageName;
|
||||||
|
|
||||||
List<NewAction> actionList;
|
List<NewAction> actionList;
|
||||||
|
|
||||||
|
List<ActionCollection> actionCollectionList;
|
||||||
|
|
||||||
Map<String, DecryptedSensitiveFields> decryptedFields;
|
Map<String, DecryptedSensitiveFields> decryptedFields;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping mongoEscapedWidgets with layoutId
|
* Mapping mongoEscapedWidgets with layoutId
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ import reactor.core.publisher.Flux;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface CustomActionCollectionRepository extends AppsmithRepository<ActionCollection> {
|
public interface CustomActionCollectionRepository extends AppsmithRepository<ActionCollection> {
|
||||||
|
|
||||||
|
Flux<ActionCollection> findByApplicationId(String applicationId, AclPermission aclPermission, Sort sort);
|
||||||
|
|
||||||
Flux<ActionCollection> findByApplicationIdAndViewMode(String applicationId, boolean viewMode, AclPermission aclPermission);
|
Flux<ActionCollection> findByApplicationIdAndViewMode(String applicationId, boolean viewMode, AclPermission aclPermission);
|
||||||
|
|
||||||
Flux<ActionCollection> findAllActionCollectionsByNameAndPageIdsAndViewMode(String name, List<String> pageIds, boolean viewMode, AclPermission aclPermission, Sort sort);
|
Flux<ActionCollection> findAllActionCollectionsByNameAndPageIdsAndViewMode(String name, List<String> pageIds, boolean viewMode, AclPermission aclPermission, Sort sort);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,15 @@ public class CustomActionCollectionRepositoryImpl extends BaseAppsmithRepository
|
||||||
super(mongoOperations, mongoConverter);
|
super(mongoOperations, mongoConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<ActionCollection> findByApplicationId(String applicationId, AclPermission aclPermission, Sort sort) {
|
||||||
|
|
||||||
|
Criteria applicationCriteria = where(fieldName(QActionCollection.actionCollection.applicationId)).is(applicationId);
|
||||||
|
|
||||||
|
return queryAll(List.of(applicationCriteria), aclPermission, sort);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<ActionCollection> findByApplicationIdAndViewMode(String applicationId, boolean viewMode, AclPermission aclPermission) {
|
public Flux<ActionCollection> findByApplicationIdAndViewMode(String applicationId, boolean viewMode, AclPermission aclPermission) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ public interface ActionCollectionService extends CrudService<ActionCollection, S
|
||||||
|
|
||||||
Flux<ActionCollectionDTO> getActionCollectionsByViewMode(MultiValueMap<String, String> params, Boolean viewMode);
|
Flux<ActionCollectionDTO> getActionCollectionsByViewMode(MultiValueMap<String, String> params, Boolean viewMode);
|
||||||
|
|
||||||
|
|
||||||
Mono<ActionCollectionDTO> update(String id, ActionCollectionDTO actionCollectionDTO);
|
Mono<ActionCollectionDTO> update(String id, ActionCollectionDTO actionCollectionDTO);
|
||||||
|
|
||||||
Mono<ActionCollectionDTO> deleteUnpublishedActionCollection(String id);
|
Mono<ActionCollectionDTO> deleteUnpublishedActionCollection(String id);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -183,7 +183,6 @@ public class ActionCollectionServiceTest {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
ActionCollectionDTO actionCollectionDTO = new ActionCollectionDTO();
|
ActionCollectionDTO actionCollectionDTO = new ActionCollectionDTO();
|
||||||
actionCollectionDTO.setName("validActionCollection");
|
|
||||||
actionCollectionDTO.setName("testActionCollection");
|
actionCollectionDTO.setName("testActionCollection");
|
||||||
actionCollectionDTO.setApplicationId(testApp.getId());
|
actionCollectionDTO.setApplicationId(testApp.getId());
|
||||||
actionCollectionDTO.setOrganizationId(testApp.getOrganizationId());
|
actionCollectionDTO.setOrganizationId(testApp.getOrganizationId());
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,17 @@ package com.appsmith.server.solutions;
|
||||||
|
|
||||||
import com.appsmith.external.models.ActionConfiguration;
|
import com.appsmith.external.models.ActionConfiguration;
|
||||||
import com.appsmith.external.models.DBAuth;
|
import com.appsmith.external.models.DBAuth;
|
||||||
|
import com.appsmith.external.models.Datasource;
|
||||||
import com.appsmith.external.models.DatasourceConfiguration;
|
import com.appsmith.external.models.DatasourceConfiguration;
|
||||||
import com.appsmith.external.models.DecryptedSensitiveFields;
|
import com.appsmith.external.models.DecryptedSensitiveFields;
|
||||||
import com.appsmith.external.models.Policy;
|
import com.appsmith.external.models.Policy;
|
||||||
import com.appsmith.external.models.Property;
|
import com.appsmith.external.models.Property;
|
||||||
import com.appsmith.server.constants.FieldName;
|
import com.appsmith.server.constants.FieldName;
|
||||||
import com.appsmith.server.constants.SerialiseApplicationObjective;
|
import com.appsmith.server.constants.SerialiseApplicationObjective;
|
||||||
|
import com.appsmith.server.domains.ActionCollection;
|
||||||
import com.appsmith.server.domains.Application;
|
import com.appsmith.server.domains.Application;
|
||||||
import com.appsmith.server.domains.ApplicationJson;
|
import com.appsmith.server.domains.ApplicationJson;
|
||||||
import com.appsmith.server.domains.ApplicationPage;
|
import com.appsmith.server.domains.ApplicationPage;
|
||||||
import com.appsmith.external.models.Datasource;
|
|
||||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||||
import com.appsmith.server.domains.Layout;
|
import com.appsmith.server.domains.Layout;
|
||||||
import com.appsmith.server.domains.NewAction;
|
import com.appsmith.server.domains.NewAction;
|
||||||
|
|
@ -19,6 +20,7 @@ import com.appsmith.server.domains.NewPage;
|
||||||
import com.appsmith.server.domains.Organization;
|
import com.appsmith.server.domains.Organization;
|
||||||
import com.appsmith.server.domains.Plugin;
|
import com.appsmith.server.domains.Plugin;
|
||||||
import com.appsmith.server.domains.PluginType;
|
import com.appsmith.server.domains.PluginType;
|
||||||
|
import com.appsmith.server.dtos.ActionCollectionDTO;
|
||||||
import com.appsmith.server.dtos.ActionDTO;
|
import com.appsmith.server.dtos.ActionDTO;
|
||||||
import com.appsmith.server.dtos.PageDTO;
|
import com.appsmith.server.dtos.PageDTO;
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
import com.appsmith.server.exceptions.AppsmithError;
|
||||||
|
|
@ -27,10 +29,12 @@ import com.appsmith.server.helpers.MockPluginExecutor;
|
||||||
import com.appsmith.server.helpers.PluginExecutorHelper;
|
import com.appsmith.server.helpers.PluginExecutorHelper;
|
||||||
import com.appsmith.server.repositories.NewPageRepository;
|
import com.appsmith.server.repositories.NewPageRepository;
|
||||||
import com.appsmith.server.repositories.PluginRepository;
|
import com.appsmith.server.repositories.PluginRepository;
|
||||||
|
import com.appsmith.server.services.ActionCollectionService;
|
||||||
import com.appsmith.server.services.ApplicationPageService;
|
import com.appsmith.server.services.ApplicationPageService;
|
||||||
import com.appsmith.server.services.ApplicationService;
|
import com.appsmith.server.services.ApplicationService;
|
||||||
import com.appsmith.server.services.DatasourceService;
|
import com.appsmith.server.services.DatasourceService;
|
||||||
import com.appsmith.server.services.LayoutActionService;
|
import com.appsmith.server.services.LayoutActionService;
|
||||||
|
import com.appsmith.server.services.LayoutCollectionService;
|
||||||
import com.appsmith.server.services.NewActionService;
|
import com.appsmith.server.services.NewActionService;
|
||||||
import com.appsmith.server.services.NewPageService;
|
import com.appsmith.server.services.NewPageService;
|
||||||
import com.appsmith.server.services.OrganizationService;
|
import com.appsmith.server.services.OrganizationService;
|
||||||
|
|
@ -70,6 +74,7 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS;
|
import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS;
|
||||||
|
import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
|
||||||
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
|
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
|
||||||
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
|
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
|
||||||
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
|
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
|
||||||
|
|
@ -116,10 +121,16 @@ public class ImportExportApplicationServiceTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private LayoutActionService layoutActionService;
|
private LayoutActionService layoutActionService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NewPageRepository newPageRepository;
|
private NewPageRepository newPageRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LayoutCollectionService layoutCollectionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActionCollectionService actionCollectionService;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
private PluginExecutorHelper pluginExecutorHelper;
|
private PluginExecutorHelper pluginExecutorHelper;
|
||||||
|
|
||||||
|
|
@ -127,8 +138,10 @@ public class ImportExportApplicationServiceTests {
|
||||||
private Plugin installedPlugin;
|
private Plugin installedPlugin;
|
||||||
private String orgId;
|
private String orgId;
|
||||||
private String testAppId;
|
private String testAppId;
|
||||||
|
private Datasource jsDatasource;
|
||||||
private Map<String, Datasource> datasourceMap = new HashMap<>();
|
private Map<String, Datasource> datasourceMap = new HashMap<>();
|
||||||
|
private Plugin installedJsPlugin;
|
||||||
|
|
||||||
private Flux<ActionDTO> getActionsInApplication(Application application) {
|
private Flux<ActionDTO> getActionsInApplication(Application application) {
|
||||||
return newPageService
|
return newPageService
|
||||||
// fetch the unpublished pages
|
// fetch the unpublished pages
|
||||||
|
|
@ -178,6 +191,13 @@ public class ImportExportApplicationServiceTests {
|
||||||
auth.setPassword("awesome-password");
|
auth.setPassword("awesome-password");
|
||||||
ds2.getDatasourceConfiguration().setAuthentication(auth);
|
ds2.getDatasourceConfiguration().setAuthentication(auth);
|
||||||
|
|
||||||
|
jsDatasource = new Datasource();
|
||||||
|
jsDatasource.setName("Default JS datasource");
|
||||||
|
jsDatasource.setOrganizationId(orgId);
|
||||||
|
installedJsPlugin = pluginRepository.findByPackageName("installed-js-plugin").block();
|
||||||
|
assert installedJsPlugin != null;
|
||||||
|
jsDatasource.setPluginId(installedJsPlugin.getId());
|
||||||
|
|
||||||
datasourceMap.put("DS1", ds1);
|
datasourceMap.put("DS1", ds1);
|
||||||
datasourceMap.put("DS2", ds2);
|
datasourceMap.put("DS2", ds2);
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +367,21 @@ public class ImportExportApplicationServiceTests {
|
||||||
action.setActionConfiguration(actionConfiguration);
|
action.setActionConfiguration(actionConfiguration);
|
||||||
action.setDatasource(ds2);
|
action.setDatasource(ds2);
|
||||||
|
|
||||||
return layoutActionService.createSingleAction(action)
|
ActionCollectionDTO actionCollectionDTO1 = new ActionCollectionDTO();
|
||||||
|
actionCollectionDTO1.setName("testCollection1");
|
||||||
|
actionCollectionDTO1.setPageId(testPage.getId());
|
||||||
|
actionCollectionDTO1.setApplicationId(testApp.getId());
|
||||||
|
actionCollectionDTO1.setOrganizationId(testApp.getOrganizationId());
|
||||||
|
actionCollectionDTO1.setPluginId(jsDatasource.getPluginId());
|
||||||
|
ActionDTO action1 = new ActionDTO();
|
||||||
|
action1.setName("testAction1");
|
||||||
|
action1.setActionConfiguration(new ActionConfiguration());
|
||||||
|
action1.getActionConfiguration().setBody("mockBody");
|
||||||
|
actionCollectionDTO1.setActions(List.of(action1));
|
||||||
|
actionCollectionDTO1.setPluginType(PluginType.JS);
|
||||||
|
|
||||||
|
return layoutCollectionService.createCollection(actionCollectionDTO1)
|
||||||
|
.then(layoutActionService.createSingleAction(action))
|
||||||
.flatMap(createdAction -> newActionService.findById(createdAction.getId(), READ_ACTIONS))
|
.flatMap(createdAction -> newActionService.findById(createdAction.getId(), READ_ACTIONS))
|
||||||
.flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
|
.flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false))
|
||||||
.then(importExportApplicationService.exportApplicationById(testApp.getId()));
|
.then(importExportApplicationService.exportApplicationById(testApp.getId()));
|
||||||
|
|
@ -360,6 +394,7 @@ public class ImportExportApplicationServiceTests {
|
||||||
Application exportedApp = applicationJson.getExportedApplication();
|
Application exportedApp = applicationJson.getExportedApplication();
|
||||||
List<NewPage> pageList = applicationJson.getPageList();
|
List<NewPage> pageList = applicationJson.getPageList();
|
||||||
List<NewAction> actionList = applicationJson.getActionList();
|
List<NewAction> actionList = applicationJson.getActionList();
|
||||||
|
List<ActionCollection> actionCollectionList = applicationJson.getActionCollectionList();
|
||||||
List<Datasource> datasourceList = applicationJson.getDatasourceList();
|
List<Datasource> datasourceList = applicationJson.getDatasourceList();
|
||||||
|
|
||||||
NewPage defaultPage = pageList.get(0);
|
NewPage defaultPage = pageList.get(0);
|
||||||
|
|
@ -367,7 +402,7 @@ public class ImportExportApplicationServiceTests {
|
||||||
assertThat(exportedApp.getName()).isEqualTo(testApplication.getName());
|
assertThat(exportedApp.getName()).isEqualTo(testApplication.getName());
|
||||||
assertThat(exportedApp.getOrganizationId()).isNull();
|
assertThat(exportedApp.getOrganizationId()).isNull();
|
||||||
assertThat(exportedApp.getPages()).isNull();
|
assertThat(exportedApp.getPages()).isNull();
|
||||||
|
|
||||||
assertThat(exportedApp.getPolicies()).hasSize(0);
|
assertThat(exportedApp.getPolicies()).hasSize(0);
|
||||||
|
|
||||||
assertThat(pageList).hasSize(1);
|
assertThat(pageList).hasSize(1);
|
||||||
|
|
@ -377,7 +412,10 @@ public class ImportExportApplicationServiceTests {
|
||||||
assertThat(defaultPage.getPolicies()).isEmpty();
|
assertThat(defaultPage.getPolicies()).isEmpty();
|
||||||
|
|
||||||
assertThat(actionList.isEmpty()).isFalse();
|
assertThat(actionList.isEmpty()).isFalse();
|
||||||
NewAction validAction = actionList.get(0);
|
assertThat(actionList).hasSize(2);
|
||||||
|
NewAction validAction = actionList.get(0).getPluginType().equals(PluginType.JS) ?
|
||||||
|
actionList.get(1) :
|
||||||
|
actionList.get(0);
|
||||||
assertThat(validAction.getApplicationId()).isNull();
|
assertThat(validAction.getApplicationId()).isNull();
|
||||||
assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName());
|
assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName());
|
||||||
assertThat(validAction.getPluginType()).isEqualTo(PluginType.API);
|
assertThat(validAction.getPluginType()).isEqualTo(PluginType.API);
|
||||||
|
|
@ -388,6 +426,18 @@ public class ImportExportApplicationServiceTests {
|
||||||
assertThat(unpublishedAction.getPageId()).isEqualTo(defaultPage.getUnpublishedPage().getName());
|
assertThat(unpublishedAction.getPageId()).isEqualTo(defaultPage.getUnpublishedPage().getName());
|
||||||
assertThat(unpublishedAction.getDatasource().getPluginId()).isEqualTo(installedPlugin.getPackageName());
|
assertThat(unpublishedAction.getDatasource().getPluginId()).isEqualTo(installedPlugin.getPackageName());
|
||||||
|
|
||||||
|
assertThat(actionCollectionList.isEmpty()).isFalse();
|
||||||
|
assertThat(actionCollectionList).hasSize(1);
|
||||||
|
final ActionCollection actionCollection = actionCollectionList.get(0);
|
||||||
|
assertThat(actionCollection.getApplicationId()).isNull();
|
||||||
|
assertThat(actionCollection.getOrganizationId()).isNull();
|
||||||
|
assertThat(actionCollection.getPolicies()).isNull();
|
||||||
|
assertThat(actionCollection.getId()).isNotNull();
|
||||||
|
assertThat(actionCollection.getUnpublishedCollection().getPluginType()).isEqualTo(PluginType.JS);
|
||||||
|
assertThat(actionCollection.getUnpublishedCollection().getPageId())
|
||||||
|
.isEqualTo(defaultPage.getUnpublishedPage().getName());
|
||||||
|
assertThat(actionCollection.getUnpublishedCollection().getPluginId()).isEqualTo(installedJsPlugin.getPackageName());
|
||||||
|
|
||||||
assertThat(datasourceList).hasSize(1);
|
assertThat(datasourceList).hasSize(1);
|
||||||
Datasource datasource = datasourceList.get(0);
|
Datasource datasource = datasourceList.get(0);
|
||||||
assertThat(datasource.getOrganizationId()).isNull();
|
assertThat(datasource.getOrganizationId()).isNull();
|
||||||
|
|
@ -593,17 +643,19 @@ public class ImportExportApplicationServiceTests {
|
||||||
StepVerifier
|
StepVerifier
|
||||||
.create(resultMono
|
.create(resultMono
|
||||||
.flatMap(application -> Mono.zip(
|
.flatMap(application -> Mono.zip(
|
||||||
Mono.just(application),
|
Mono.just(application),
|
||||||
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
|
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
|
||||||
getActionsInApplication(application).collectList(),
|
getActionsInApplication(application).collectList(),
|
||||||
newPageService.findByApplicationId(application.getId(), MANAGE_PAGES, false).collectList()
|
newPageService.findByApplicationId(application.getId(), MANAGE_PAGES, false).collectList(),
|
||||||
|
actionCollectionService.findAllByApplicationIdAndViewMode(application.getId(), false, MANAGE_ACTIONS, null).collectList()
|
||||||
)))
|
)))
|
||||||
.assertNext(tuple -> {
|
.assertNext(tuple -> {
|
||||||
final Application application = tuple.getT1();
|
final Application application = tuple.getT1();
|
||||||
final List<Datasource> datasourceList = tuple.getT2();
|
final List<Datasource> datasourceList = tuple.getT2();
|
||||||
final List<ActionDTO> actionDTOS = tuple.getT3();
|
final List<ActionDTO> actionDTOS = tuple.getT3();
|
||||||
final List<PageDTO> pageList = tuple.getT4();
|
final List<PageDTO> pageList = tuple.getT4();
|
||||||
|
final List<ActionCollection> actionCollectionList = tuple.getT5();
|
||||||
|
|
||||||
assertThat(application.getName()).isEqualTo("valid_application");
|
assertThat(application.getName()).isEqualTo("valid_application");
|
||||||
assertThat(application.getOrganizationId()).isNotNull();
|
assertThat(application.getOrganizationId()).isNotNull();
|
||||||
assertThat(application.getPages()).hasSize(2);
|
assertThat(application.getPages()).hasSize(2);
|
||||||
|
|
@ -624,20 +676,25 @@ public class ImportExportApplicationServiceTests {
|
||||||
assertThat(auth.getUsername()).isNotNull();
|
assertThat(auth.getUsername()).isNotNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
assertThat(actionDTOS).isNotEmpty();
|
assertThat(actionDTOS).isNotEmpty();
|
||||||
actionDTOS.forEach(actionDTO -> {
|
actionDTOS.forEach(actionDTO -> {
|
||||||
assertThat(actionDTO.getPageId()).isNotEqualTo(pageList.get(0).getName());
|
assertThat(actionDTO.getPageId()).isNotEqualTo(pageList.get(0).getName());
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertThat(actionCollectionList).isNotEmpty();
|
||||||
|
actionCollectionList.forEach(actionCollection -> {
|
||||||
|
assertThat(actionCollection.getUnpublishedCollection().getPageId()).isNotEqualTo(pageList.get(0).getName());
|
||||||
|
});
|
||||||
|
|
||||||
assertThat(pageList).hasSize(2);
|
assertThat(pageList).hasSize(2);
|
||||||
|
|
||||||
ApplicationPage defaultAppPage = application.getPages()
|
ApplicationPage defaultAppPage = application.getPages()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(ApplicationPage::getIsDefault)
|
.filter(ApplicationPage::getIsDefault)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
assertThat(defaultAppPage).isNotNull();
|
assertThat(defaultAppPage).isNotNull();
|
||||||
|
|
||||||
PageDTO defaultPageDTO = pageList.stream()
|
PageDTO defaultPageDTO = pageList.stream()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user