mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
64961: Edit bitstream page foundations
This commit is contained in:
@@ -84,6 +84,13 @@
|
|||||||
"auth.messages.expired": "Your session has expired. Please log in again.",
|
"auth.messages.expired": "Your session has expired. Please log in again.",
|
||||||
"bitstream.edit.title": "Edit bitstream",
|
"bitstream.edit.title": "Edit bitstream",
|
||||||
"bitstream.edit.bitstream": "Bitstream: ",
|
"bitstream.edit.bitstream": "Bitstream: ",
|
||||||
|
"bitstream.edit.form.description.label": "Description",
|
||||||
|
"bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"Main article\" or \"Experiment data readings\".",
|
||||||
|
"bitstream.edit.form.embargo.label": "Embargo until specific date",
|
||||||
|
"bitstream.edit.form.embargo.hint": "The first day from which access is allowed. <b>This date cannot be modified on this form.</b> To set an embargo date for a bitstream, go to the <i>Item Status</i> tab, click <i>Authorizations...<i/>, create or edit the bitstream's <i>READ</i> policy, and set the <i>Start Date</i> as desired.",
|
||||||
|
"bitstream.edit.form.fileName.label": "Filename",
|
||||||
|
"bitstream.edit.form.fileName.hint": "Change the filename for the bitstream. Note that this will change the display bitstream URL, but old links will still resolve as long as the sequence ID does not change.",
|
||||||
|
"bitstream.edit.form.primaryBitstream.label": "Primary bitstream",
|
||||||
"browse.comcol.by.author": "By Author",
|
"browse.comcol.by.author": "By Author",
|
||||||
"browse.comcol.by.dateissued": "By Issue Date",
|
"browse.comcol.by.dateissued": "By Issue Date",
|
||||||
"browse.comcol.by.subject": "By Subject",
|
"browse.comcol.by.subject": "By Subject",
|
||||||
@@ -733,4 +740,4 @@
|
|||||||
"uploader.or": ", or",
|
"uploader.or": ", or",
|
||||||
"uploader.processing": "Processing",
|
"uploader.processing": "Processing",
|
||||||
"uploader.queue-lenght": "Queue length"
|
"uploader.queue-lenght": "Queue length"
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,22 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mb-4">
|
<div class="col-md-2">
|
||||||
<h2>{{'bitstream.edit.title' | translate}}</h2>
|
<ds-thumbnail [thumbnail]="bitstream"></ds-thumbnail>
|
||||||
<ng-container *ngVar="(bitstreamRD$ | async)?.payload as bitstream">
|
</div>
|
||||||
<div *ngIf="bitstream">
|
<div class="col-md-10">
|
||||||
<span class="font-weight-bold">{{'bitstream.edit.bitstream' | translate}}</span>
|
<div class="container">
|
||||||
<span>{{bitstream.name}}</span>
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3>{{bitstream?.name}} <span class="text-muted">({{bitstream?.sizeBytes | dsFileSize}})</span></h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</div>
|
||||||
|
<ds-form *ngIf="bitstream"
|
||||||
|
[formId]="'edit-bitstream-form-id'"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[formLayout]="formLayout"
|
||||||
|
(submitForm)="onSubmit()"></ds-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
::ng-deep {
|
||||||
|
.switch {
|
||||||
|
position: absolute;
|
||||||
|
top: $spacer*2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,30 +1,248 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import {
|
||||||
|
DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService,
|
||||||
|
DynamicInputModel,
|
||||||
|
DynamicTextAreaModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-bitstream-page',
|
selector: 'ds-edit-bitstream-page',
|
||||||
|
styleUrls: ['./edit-bitstream-page.component.scss'],
|
||||||
templateUrl: './edit-bitstream-page.component.html',
|
templateUrl: './edit-bitstream-page.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Page component for editing a bitstream
|
* Page component for editing a bitstream
|
||||||
*/
|
*/
|
||||||
export class EditBitstreamPageComponent implements OnInit {
|
export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bitstream to edit
|
* The bitstream to edit
|
||||||
*/
|
*/
|
||||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
bitstream: Bitstream;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {
|
/**
|
||||||
|
* @type {string} Key prefix used to generate form messages
|
||||||
|
*/
|
||||||
|
KEY_PREFIX = 'bitstream.edit.form.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} Key suffix used to generate form labels
|
||||||
|
*/
|
||||||
|
LABEL_KEY_SUFFIX = '.label';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} Key suffix used to generate form labels
|
||||||
|
*/
|
||||||
|
HINT_KEY_SUFFIX = '.hint';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dynamic Input Model for the file's name
|
||||||
|
*/
|
||||||
|
fileNameModel = new DynamicInputModel({
|
||||||
|
id: 'fileName',
|
||||||
|
name: 'fileName',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'You must provide a file name for the bitstream'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dynamic Switch Model for the file's name
|
||||||
|
*/
|
||||||
|
primaryBitstreamModel = new DynamicCustomSwitchModel({
|
||||||
|
id: 'primaryBitstream',
|
||||||
|
name: 'primaryBitstream'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dynamic TextArea Model for the file's description
|
||||||
|
*/
|
||||||
|
descriptionModel = new DynamicTextAreaModel({
|
||||||
|
id: 'description',
|
||||||
|
name: 'description'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dynamic Input Model for the file's embargo (disabled on this page)
|
||||||
|
*/
|
||||||
|
embargoModel = new DynamicInputModel({
|
||||||
|
id: 'embargo',
|
||||||
|
name: 'embargo',
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All input models in a simple array for easier iterations
|
||||||
|
*/
|
||||||
|
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dynamic form fields used for editing the information of a bitstream
|
||||||
|
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicFormGroupModel({
|
||||||
|
id: 'fileNamePrimaryContainer',
|
||||||
|
group: [
|
||||||
|
this.fileNameModel,
|
||||||
|
this.primaryBitstreamModel
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new DynamicFormGroupModel({
|
||||||
|
id: 'descriptionContainer',
|
||||||
|
group: [
|
||||||
|
this.descriptionModel
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new DynamicFormGroupModel({
|
||||||
|
id: 'embargoContainer',
|
||||||
|
group: [
|
||||||
|
this.embargoModel
|
||||||
|
]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout used for structuring the form inputs
|
||||||
|
*/
|
||||||
|
formLayout: DynamicFormLayout = {
|
||||||
|
fileName: {
|
||||||
|
grid: {
|
||||||
|
host: 'col col-sm-8 d-inline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
primaryBitstream: {
|
||||||
|
grid: {
|
||||||
|
host: 'col col-sm-4 d-inline-block switch'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
grid: {
|
||||||
|
host: 'col-12 d-inline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
embargo: {
|
||||||
|
grid: {
|
||||||
|
host: 'col-12 d-inline-block'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fileNamePrimaryContainer: {
|
||||||
|
grid: {
|
||||||
|
host: 'row position-relative'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
descriptionContainer: {
|
||||||
|
grid: {
|
||||||
|
host: 'row'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
embargoContainer: {
|
||||||
|
grid: {
|
||||||
|
host: 'row'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form group of this form
|
||||||
|
*/
|
||||||
|
formGroup: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subscription on the bitstream
|
||||||
|
*/
|
||||||
|
sub: Subscription;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private formService: DynamicFormService,
|
||||||
|
private translate: TranslateService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component
|
||||||
|
* - Create a FormGroup using the FormModel defined earlier
|
||||||
|
* - Subscribe on the route data to fetch the bitstream to edit and update the form values
|
||||||
|
* - Translate the form labels and hints
|
||||||
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream));
|
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||||
|
this.sub = this.route.data.pipe(map((data) => data.bitstream)).subscribe((bitstreamRD) => {
|
||||||
|
this.bitstream = bitstreamRD.payload;
|
||||||
|
this.updateForm(this.bitstream);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
this.translate.onLangChange
|
||||||
|
.subscribe(() => {
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current form values with bitstream properties
|
||||||
|
* @param bitstream
|
||||||
|
*/
|
||||||
|
updateForm(bitstream: Bitstream) {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
fileNamePrimaryContainer: {
|
||||||
|
fileName: bitstream.name,
|
||||||
|
primaryBitstream: false
|
||||||
|
},
|
||||||
|
descriptionContainer: {
|
||||||
|
description: bitstream.description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to update translations of labels and hints on init and on language change
|
||||||
|
*/
|
||||||
|
private updateFieldTranslations() {
|
||||||
|
this.inputModels.forEach(
|
||||||
|
(fieldModel: DynamicFormControlModel) => {
|
||||||
|
this.updateFieldTranslation(fieldModel);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the translations of a DynamicFormControlModel
|
||||||
|
* @param fieldModel
|
||||||
|
*/
|
||||||
|
private updateFieldTranslation(fieldModel) {
|
||||||
|
fieldModel.label = this.translate.instant(this.KEY_PREFIX + fieldModel.id + this.LABEL_KEY_SUFFIX);
|
||||||
|
if (fieldModel.id !== this.primaryBitstreamModel.id) {
|
||||||
|
fieldModel.hint = this.translate.instant(this.KEY_PREFIX + fieldModel.id + this.HINT_KEY_SUFFIX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for changes against the bitstream and send update requests to the REST API
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
// TODO: Check for changes against the bitstream and send requests to the REST API accordingly
|
||||||
|
console.log(this.formGroup.getRawValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from open subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.sub) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -68,6 +68,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a
|
|||||||
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
|
import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components';
|
||||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model';
|
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model';
|
||||||
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
|
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
|
||||||
|
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
|
||||||
|
import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component';
|
||||||
|
|
||||||
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
|
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
|
||||||
switch (model.type) {
|
switch (model.type) {
|
||||||
@@ -125,6 +127,9 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<
|
|||||||
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME:
|
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME:
|
||||||
return DsDynamicLookupComponent;
|
return DsDynamicLookupComponent;
|
||||||
|
|
||||||
|
case DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH:
|
||||||
|
return CustomSwitchComponent;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -176,6 +181,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
super(componentFactoryResolver, layoutService, validationService);
|
super(componentFactoryResolver, layoutService, validationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isCheckbox(): boolean {
|
||||||
|
return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
if (changes) {
|
if (changes) {
|
||||||
super.ngOnChanges(changes);
|
super.ngOnChanges(changes);
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
<div [formGroup]="group" class="form-check custom-control custom-switch" [class.disabled]="model.disabled">
|
||||||
|
<input type="checkbox" class="form-check-input custom-control-input"
|
||||||
|
[checked]="model.checked"
|
||||||
|
[class.is-invalid]="showErrorMessages"
|
||||||
|
[dynamicId]="bindId && model.id"
|
||||||
|
[formControlName]="model.id"
|
||||||
|
[indeterminate]="model.indeterminate"
|
||||||
|
[name]="model.name"
|
||||||
|
[ngClass]="getClass('element', 'control')"
|
||||||
|
[required]="model.required"
|
||||||
|
[tabindex]="model.tabIndex"
|
||||||
|
[value]="model.value"
|
||||||
|
(blur)="onBlur($event)"
|
||||||
|
(change)="onChange($event)"
|
||||||
|
(focus)="onFocus($event)"/>
|
||||||
|
<label class="form-check-label custom-control-label" [for]="bindId && model.id">
|
||||||
|
<span [innerHTML]="model.label"
|
||||||
|
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
@@ -0,0 +1,20 @@
|
|||||||
|
import { DynamicNGBootstrapCheckboxComponent } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { DynamicCustomSwitchModel } from './custom-switch.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-custom-switch',
|
||||||
|
styleUrls: ['./custom-switch.component.scss'],
|
||||||
|
templateUrl: './custom-switch.component.html',
|
||||||
|
})
|
||||||
|
export class CustomSwitchComponent extends DynamicNGBootstrapCheckboxComponent {
|
||||||
|
@Input() bindId = true;
|
||||||
|
@Input() group: FormGroup;
|
||||||
|
@Input() model: DynamicCustomSwitchModel;
|
||||||
|
@Output() selected = new EventEmitter<number>();
|
||||||
|
@Output() remove = new EventEmitter<number>();
|
||||||
|
@Output() blur = new EventEmitter<any>();
|
||||||
|
@Output() change = new EventEmitter<any>();
|
||||||
|
@Output() focus = new EventEmitter<any>();
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import {
|
||||||
|
DynamicCheckboxModel,
|
||||||
|
DynamicCheckboxModelConfig,
|
||||||
|
DynamicFormControlLayout,
|
||||||
|
serializable
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
|
export const DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH = 'CUSTOM_SWITCH';
|
||||||
|
|
||||||
|
export class DynamicCustomSwitchModel extends DynamicCheckboxModel {
|
||||||
|
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH;
|
||||||
|
|
||||||
|
constructor(config: DynamicCheckboxModelConfig, layout?: DynamicFormControlLayout) {
|
||||||
|
super(config, layout);
|
||||||
|
}
|
||||||
|
}
|
@@ -142,6 +142,7 @@ import { AbstractTrackableComponent } from './trackable/abstract-trackable.compo
|
|||||||
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
||||||
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
|
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
|
||||||
|
import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -265,7 +266,8 @@ const COMPONENTS = [
|
|||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
ItemTypeBadgeComponent,
|
ItemTypeBadgeComponent,
|
||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
AbstractTrackableComponent
|
AbstractTrackableComponent,
|
||||||
|
CustomSwitchComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -309,7 +311,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
StartsWithTextComponent,
|
StartsWithTextComponent,
|
||||||
PlainTextMetadataListElementComponent,
|
PlainTextMetadataListElementComponent,
|
||||||
ItemMetadataListElementComponent,
|
ItemMetadataListElementComponent,
|
||||||
MetadataRepresentationListElementComponent
|
MetadataRepresentationListElementComponent,
|
||||||
|
CustomSwitchComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||||
|
Reference in New Issue
Block a user