Merge branch 'fullydeleable' into 'master'
Fully Deletable Videos Closes #74 See merge request lukas/openmediacenter!52
This commit is contained in:
		| @@ -15,7 +15,8 @@ func readVideosFromResultset(rows *sql.Rows) []types.VideoUnloadedType { | ||||
| 		var vid types.VideoUnloadedType | ||||
| 		err := rows.Scan(&vid.MovieId, &vid.MovieName) | ||||
| 		if err != nil { | ||||
| 			panic(err.Error()) // proper error handling instead of panic in your app | ||||
| 			fmt.Println(err.Error()) | ||||
| 			return nil | ||||
| 		} | ||||
| 		result = append(result, vid) | ||||
| 	} | ||||
|   | ||||
| @@ -67,12 +67,13 @@ func getSettingsFromDB() { | ||||
| 		sett := settings.LoadSettings() | ||||
|  | ||||
| 		type InitialDataTypeResponse struct { | ||||
| 			DarkMode        bool | ||||
| 			Pasword         bool | ||||
| 			MediacenterName string | ||||
| 			VideoPath       string | ||||
| 			TVShowPath      string | ||||
| 			TVShowEnabled   bool | ||||
| 			DarkMode          bool | ||||
| 			Pasword           bool | ||||
| 			MediacenterName   string | ||||
| 			VideoPath         string | ||||
| 			TVShowPath        string | ||||
| 			TVShowEnabled     bool | ||||
| 			FullDeleteEnabled bool | ||||
| 		} | ||||
|  | ||||
| 		regexMatchUrl := regexp.MustCompile("^http(|s)://([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}") | ||||
| @@ -82,12 +83,13 @@ func getSettingsFromDB() { | ||||
| 		serverTVShowPath := strings.TrimPrefix(sett.TVShowPath, tvshowurl) | ||||
|  | ||||
| 		res := InitialDataTypeResponse{ | ||||
| 			DarkMode:        sett.DarkMode, | ||||
| 			Pasword:         sett.Pasword != "-1", | ||||
| 			MediacenterName: sett.MediacenterName, | ||||
| 			VideoPath:       serverVideoPath, | ||||
| 			TVShowPath:      serverTVShowPath, | ||||
| 			TVShowEnabled:   settings.TVShowsEnabled(), | ||||
| 			DarkMode:          sett.DarkMode, | ||||
| 			Pasword:           sett.Pasword != "-1", | ||||
| 			MediacenterName:   sett.MediacenterName, | ||||
| 			VideoPath:         serverVideoPath, | ||||
| 			TVShowPath:        serverTVShowPath, | ||||
| 			TVShowEnabled:     settings.TVShowsEnabled(), | ||||
| 			FullDeleteEnabled: settings.VideosDeletable(), | ||||
| 		} | ||||
|  | ||||
| 		str, _ := json.Marshal(res) | ||||
|   | ||||
| @@ -6,6 +6,8 @@ import ( | ||||
| 	"net/url" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/database/settings" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| ) | ||||
|  | ||||
| @@ -418,12 +420,14 @@ func addToVideoHandlers() { | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {int} MovieId ID of video | ||||
| 	 * @apiParam {bool} FullyDelete Delete video from disk? | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or error message if not | ||||
| 	 */ | ||||
| 	AddHandler("deleteVideo", VideoNode, func(info *HandlerInfo) []byte { | ||||
| 		var args struct { | ||||
| 			MovieId int | ||||
| 			MovieId     int | ||||
| 			FullyDelete bool | ||||
| 		} | ||||
| 		if err := FillStruct(&args, info.Data); err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| @@ -443,6 +447,26 @@ func addToVideoHandlers() { | ||||
| 			return database.ManualSuccessResponse(err) | ||||
| 		} | ||||
|  | ||||
| 		// only allow deletion of video if cli flag is set, independent of passed api arg | ||||
| 		if settings.VideosDeletable() && args.FullyDelete { | ||||
| 			// get physical path of video to delete | ||||
| 			query = fmt.Sprintf("SELECT movie_url FROM videos WHERE movie_id=%d", args.MovieId) | ||||
| 			var vidpath string | ||||
| 			err := database.QueryRow(query).Scan(&vidpath) | ||||
| 			if err != nil { | ||||
| 				return database.ManualSuccessResponse(err) | ||||
| 			} | ||||
|  | ||||
| 			assembledPath := database.SettingsVideoPrefix + "/" + vidpath | ||||
|  | ||||
| 			err = os.Remove(assembledPath) | ||||
| 			if err != nil { | ||||
| 				fmt.Printf("unable to delete file: %s -- %s\n", assembledPath, err.Error()) | ||||
| 				return database.ManualSuccessResponse(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// delete video row from db | ||||
| 		query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId) | ||||
| 		return database.SuccessQuery(query) | ||||
| 	}) | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package settings | ||||
|  | ||||
| var tvShowEnabled bool | ||||
| var videosDeletable bool | ||||
|  | ||||
| func TVShowsEnabled() bool { | ||||
| 	return tvShowEnabled | ||||
| @@ -9,3 +10,11 @@ func TVShowsEnabled() bool { | ||||
| func SetTVShowEnabled(enabled bool) { | ||||
| 	tvShowEnabled = enabled | ||||
| } | ||||
|  | ||||
| func VideosDeletable() bool { | ||||
| 	return videosDeletable | ||||
| } | ||||
|  | ||||
| func SetVideosDeletable(deletable bool) { | ||||
| 	videosDeletable = deletable | ||||
| } | ||||
|   | ||||
| @@ -57,10 +57,12 @@ func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) { | ||||
| 	pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex") | ||||
|  | ||||
| 	disableTVShowSupport := flag.Bool("DisableTVSupport", false, "Disable the TVShow support and pages") | ||||
| 	videosFullyDeletable := flag.Bool("FullyDeletableVideos", false, "Allow deletion from harddisk") | ||||
|  | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	settings2.SetTVShowEnabled(!*disableTVShowSupport) | ||||
| 	settings2.SetVideosDeletable(*videosFullyDeletable) | ||||
|  | ||||
| 	return &database.DatabaseConfig{ | ||||
| 		DBHost:     *dbhostPtr, | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|     "build": "CI=false react-scripts build", | ||||
|     "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default", | ||||
|     "lint": "eslint --format gitlab src/", | ||||
|     "apidoc": "apidoc -i apiGo/ -o doc/" | ||||
|     "apidoc": "apidoc --single -i apiGo/ -o doc/" | ||||
|   }, | ||||
|   "jest": { | ||||
|     "collectCoverageFrom": [ | ||||
|   | ||||
| @@ -87,6 +87,7 @@ class App extends React.Component<{}, state> { | ||||
|             GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); | ||||
|  | ||||
|             GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); | ||||
|             GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled); | ||||
|  | ||||
|             this.setState({ | ||||
|                 mediacentername: result.MediacenterName | ||||
|   | ||||
							
								
								
									
										51
									
								
								src/elements/Popups/ButtonPopup/ButtonPopup.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/elements/Popups/ButtonPopup/ButtonPopup.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import {ButtonPopup} from './ButtonPopup'; | ||||
| import exp from "constants"; | ||||
|  | ||||
| describe('<ButtonPopup/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
|         const wrapper = shallow(<ButtonPopup/>); | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('renders two buttons', function () { | ||||
|         const wrapper = shallow(<ButtonPopup/>); | ||||
|         expect(wrapper.find('Button')).toHaveLength(2); | ||||
|     }); | ||||
|  | ||||
|     it('renders three buttons if alternative defined', function () { | ||||
|         const wrapper = shallow(<ButtonPopup AlternativeButtonTitle='alt'/>); | ||||
|         expect(wrapper.find('Button')).toHaveLength(3); | ||||
|     }); | ||||
|  | ||||
|     it('test click handlings', function () { | ||||
|         const althandler = jest.fn(); | ||||
|         const denyhandler = jest.fn(); | ||||
|         const submithandler = jest.fn(); | ||||
|  | ||||
|         const wrapper = shallow(<ButtonPopup DenyButtonTitle='deny' onDeny={denyhandler} SubmitButtonTitle='submit' | ||||
|                                              onSubmit={submithandler} AlternativeButtonTitle='alt' | ||||
|                                              onAlternativeButton={althandler}/>); | ||||
|         wrapper.find('Button').findWhere(e => e.props().title === "deny").simulate('click'); | ||||
|         expect(denyhandler).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|         wrapper.find('Button').findWhere(e => e.props().title === "alt").simulate('click'); | ||||
|         expect(althandler).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|         wrapper.find('Button').findWhere(e => e.props().title === "submit").simulate('click'); | ||||
|         expect(submithandler).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
|  | ||||
|     it('test Parentsubmit and parenthide callbacks', function () { | ||||
|         const ondeny = jest.fn(); | ||||
|         const onsubmit = jest.fn(); | ||||
|  | ||||
|         const wrapper = shallow(<ButtonPopup DenyButtonTitle='deny' SubmitButtonTitle='submit' onDeny={ondeny} onSubmit={onsubmit} AlternativeButtonTitle='alt'/>); | ||||
|         wrapper.find('PopupBase').props().onHide(); | ||||
|         expect(ondeny).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|         wrapper.find('PopupBase').props().ParentSubmit(); | ||||
|         expect(onsubmit).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										58
									
								
								src/elements/Popups/ButtonPopup/ButtonPopup.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/elements/Popups/ButtonPopup/ButtonPopup.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import React from 'react'; | ||||
| import PopupBase from '../PopupBase'; | ||||
| import {Button} from '../../GPElements/Button'; | ||||
|  | ||||
| /** | ||||
|  * Delete Video popup | ||||
|  * can only be rendered once! | ||||
|  * @constructor | ||||
|  */ | ||||
| export const ButtonPopup = (props: { | ||||
|     onSubmit: () => void; | ||||
|     onDeny: () => void; | ||||
|     onAlternativeButton?: () => void; | ||||
|     SubmitButtonTitle: string; | ||||
|     DenyButtonTitle: string; | ||||
|     AlternativeButtonTitle?: string; | ||||
|     Title: string; | ||||
| }): JSX.Element => { | ||||
|     return ( | ||||
|         <> | ||||
|             <PopupBase | ||||
|                 title={props.Title} | ||||
|                 onHide={(): void => props.onDeny()} | ||||
|                 height='200px' | ||||
|                 width='400px' | ||||
|                 ParentSubmit={(): void => { | ||||
|                     props.onSubmit(); | ||||
|                 }}> | ||||
|                 <Button | ||||
|                     onClick={(): void => { | ||||
|                         props.onSubmit(); | ||||
|                     }} | ||||
|                     title={props.SubmitButtonTitle} | ||||
|                 /> | ||||
|  | ||||
|                 {props.AlternativeButtonTitle ? ( | ||||
|                     <Button | ||||
|                         color={{backgroundColor: 'darkorange'}} | ||||
|                         onClick={(): void => { | ||||
|                             props.onAlternativeButton ? props.onAlternativeButton() : null; | ||||
|                         }} | ||||
|                         title={props.AlternativeButtonTitle} | ||||
|                     /> | ||||
|                 ) : ( | ||||
|                     <></> | ||||
|                 )} | ||||
|  | ||||
|                 <Button | ||||
|                     color={{backgroundColor: 'red'}} | ||||
|                     onClick={(): void => { | ||||
|                         props.onDeny(); | ||||
|                     }} | ||||
|                     title={props.DenyButtonTitle} | ||||
|                 /> | ||||
|             </PopupBase> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @@ -2,6 +2,7 @@ import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import {Player} from './Player'; | ||||
| import {callAPI} from '../../utils/Api'; | ||||
| import GlobalInfos from "../../utils/GlobalInfos"; | ||||
|  | ||||
| describe('<Player/>', function () { | ||||
|  | ||||
| @@ -84,23 +85,54 @@ describe('<Player/>', function () { | ||||
|         expect(wrapper.find('AddTagPopup')).toHaveLength(1); | ||||
|     }); | ||||
|  | ||||
|     it('test delete button', done => { | ||||
|     it('test fully delete popup rendering', function () { | ||||
|         const wrapper = instance(); | ||||
|  | ||||
|         // allow videos to be fully deletable | ||||
|         GlobalInfos.setFullDeleteEnabled(true); | ||||
|  | ||||
|         wrapper.setProps({history: {goBack: jest.fn()}}); | ||||
|         wrapper.setState({deletepopupvisible: true}); | ||||
|  | ||||
|         global.fetch = prepareFetchApi({result: 'success'}); | ||||
|         expect(wrapper.find('ButtonPopup')).toHaveLength(1) | ||||
|     }); | ||||
|  | ||||
|     it('test delete popup rendering', function () { | ||||
|         const wrapper = instance(); | ||||
|  | ||||
|         GlobalInfos.setFullDeleteEnabled(false); | ||||
|         wrapper.setState({deletepopupvisible: true}); | ||||
|  | ||||
|         expect(wrapper.find('ButtonPopup')).toHaveLength(1) | ||||
|     }); | ||||
|  | ||||
|     it('test delete button', () => { | ||||
|         const wrapper = instance(); | ||||
|         const callback = jest.fn(); | ||||
|  | ||||
|         wrapper.setProps({history: {goBack: callback}}); | ||||
|  | ||||
|         callAPIMock({result: 'success'}) | ||||
|         GlobalInfos.setFullDeleteEnabled(false); | ||||
|  | ||||
|         // request the popup to pop | ||||
|         wrapper.find('.videoactions').find('Button').at(2).simulate('click'); | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             // refetch is called so fetch called 3 times | ||||
|             expect(global.fetch).toHaveBeenCalledTimes(1); | ||||
|             expect(wrapper.instance().props.history.goBack).toHaveBeenCalledTimes(1); | ||||
|         // click the first submit button | ||||
|         wrapper.find('ButtonPopup').dive().find('Button').at(0).simulate('click') | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         // refetch is called so fetch called 3 times | ||||
|         expect(callAPI).toHaveBeenCalledTimes(1); | ||||
|         expect(callback).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|         // now lets test if this works also with the fullydeletepopup | ||||
|         GlobalInfos.setFullDeleteEnabled(true); | ||||
|         // request the popup to pop | ||||
|         wrapper.setState({deletepopupvisible: true}, () => { | ||||
|             // click the first submit button | ||||
|             wrapper.find('ButtonPopup').dive().find('Button').at(0).simulate('click') | ||||
|  | ||||
|             expect(callAPI).toHaveBeenCalledTimes(2); | ||||
|             expect(callback).toHaveBeenCalledTimes(2); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| @@ -152,16 +184,14 @@ describe('<Player/>', function () { | ||||
|     it('test click of quickadd tag btn', done => { | ||||
|         const wrapper = generatetag(); | ||||
|  | ||||
|         global.fetch = prepareFetchApi({result: 'success'}); | ||||
|         callAPIMock({result: 'success'}) | ||||
|  | ||||
|         // render tag subcomponent | ||||
|         const tag = wrapper.find('Tag').first().dive(); | ||||
|         tag.simulate('click'); | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             expect(global.fetch).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             expect(callAPI).toHaveBeenCalledTimes(1); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
| @@ -169,7 +199,7 @@ describe('<Player/>', function () { | ||||
|     it('test failing quickadd', done => { | ||||
|         const wrapper = generatetag(); | ||||
|  | ||||
|         global.fetch = prepareFetchApi({result: 'nonsuccess'}); | ||||
|         callAPIMock({result: 'nonsuccess'}); | ||||
|         global.console.error = jest.fn(); | ||||
|  | ||||
|         // render tag subcomponent | ||||
| @@ -178,8 +208,6 @@ describe('<Player/>', function () { | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             expect(global.console.error).toHaveBeenCalledTimes(2); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import PlyrJS from 'plyr'; | ||||
| import {Button} from '../../elements/GPElements/Button'; | ||||
| import {VideoTypes} from '../../types/ApiTypes'; | ||||
| import GlobalInfos from '../../utils/GlobalInfos'; | ||||
| import {ButtonPopup} from '../../elements/Popups/ButtonPopup/ButtonPopup'; | ||||
|  | ||||
| interface Props extends RouteComponentProps<{id: string}> {} | ||||
|  | ||||
| @@ -35,6 +36,7 @@ interface mystate { | ||||
|     suggesttag: TagType[]; | ||||
|     popupvisible: boolean; | ||||
|     actorpopupvisible: boolean; | ||||
|     deletepopupvisible: boolean; | ||||
|     actors: ActorType[]; | ||||
| } | ||||
|  | ||||
| @@ -56,6 +58,7 @@ export class Player extends React.Component<Props, mystate> { | ||||
|             suggesttag: [], | ||||
|             popupvisible: false, | ||||
|             actorpopupvisible: false, | ||||
|             deletepopupvisible: false, | ||||
|             actors: [] | ||||
|         }; | ||||
|  | ||||
| @@ -91,7 +94,7 @@ export class Player extends React.Component<Props, mystate> { | ||||
|                         <Button | ||||
|                             title='Delete Video' | ||||
|                             onClick={(): void => { | ||||
|                                 this.deleteVideo(); | ||||
|                                 this.setState({deletepopupvisible: true}); | ||||
|                             }} | ||||
|                             color={{backgroundColor: 'red'}} | ||||
|                         /> | ||||
| @@ -196,10 +199,46 @@ export class Player extends React.Component<Props, mystate> { | ||||
|                         movieId={this.state.movieId} | ||||
|                     /> | ||||
|                 ) : null} | ||||
|                 {this.state.deletepopupvisible ? this.renderDeletePopup() : null} | ||||
|             </> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     renderDeletePopup(): JSX.Element { | ||||
|         if (GlobalInfos.isVideoFulldeleteable()) { | ||||
|             return ( | ||||
|                 <ButtonPopup | ||||
|                     onDeny={(): void => this.setState({deletepopupvisible: false})} | ||||
|                     onSubmit={(): void => { | ||||
|                         this.setState({deletepopupvisible: false}); | ||||
|                         this.deleteVideo(true); | ||||
|                     }} | ||||
|                     onAlternativeButton={(): void => { | ||||
|                         this.setState({deletepopupvisible: false}); | ||||
|                         this.deleteVideo(false); | ||||
|                     }} | ||||
|                     DenyButtonTitle='Cancel' | ||||
|                     SubmitButtonTitle='Fully Delete!' | ||||
|                     Title='Fully Delete Video?' | ||||
|                     AlternativeButtonTitle='Reference Only' | ||||
|                 /> | ||||
|             ); | ||||
|         } else { | ||||
|             return ( | ||||
|                 <ButtonPopup | ||||
|                     onDeny={(): void => this.setState({deletepopupvisible: false})} | ||||
|                     onSubmit={(): void => { | ||||
|                         this.setState({deletepopupvisible: false}); | ||||
|                         this.deleteVideo(false); | ||||
|                     }} | ||||
|                     DenyButtonTitle='Cancel' | ||||
|                     SubmitButtonTitle='Delete Video Reference!' | ||||
|                     Title='Delete Video?' | ||||
|                 /> | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * quick add callback to add tag to db and change gui correctly | ||||
|      * @param tagId id of tag to add | ||||
| @@ -325,17 +364,16 @@ export class Player extends React.Component<Props, mystate> { | ||||
|     /** | ||||
|      * delete the current video and return to last page | ||||
|      */ | ||||
|     deleteVideo(): void { | ||||
|     deleteVideo(fullyDelete: boolean): void { | ||||
|         callAPI( | ||||
|             APINode.Video, | ||||
|             {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10)}, | ||||
|             {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10), FullyDelete: fullyDelete}, | ||||
|             (result: GeneralSuccess) => { | ||||
|                 if (result.result === 'success') { | ||||
|                     // return to last element if successful | ||||
|                     this.props.history.goBack(); | ||||
|                 } else { | ||||
|                     console.error('an error occured while liking'); | ||||
|                     console.error(result); | ||||
|                     console.error('an error occured while deleting the video: ' + JSON.stringify(result)); | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|   | ||||
| @@ -37,6 +37,7 @@ export namespace SettingsTypes { | ||||
|         VideoPath: string; | ||||
|         TVShowPath: string; | ||||
|         TVShowEnabled: boolean; | ||||
|         FullDeleteEnabled: boolean; | ||||
|     } | ||||
|  | ||||
|     export interface SettingsType { | ||||
|   | ||||
| @@ -10,6 +10,7 @@ class StaticInfos { | ||||
|     private videopath: string = ''; | ||||
|     private tvshowpath: string = ''; | ||||
|     private TVShowsEnabled: boolean = false; | ||||
|     private fullDeleteable: boolean = false; | ||||
|  | ||||
|     /** | ||||
|      * check if the current theme is the dark theme | ||||
| @@ -80,6 +81,14 @@ class StaticInfos { | ||||
|     isTVShowEnabled(): boolean { | ||||
|         return this.TVShowsEnabled; | ||||
|     } | ||||
|  | ||||
|     setFullDeleteEnabled(FullDeleteEnabled: boolean): void { | ||||
|         this.fullDeleteable = FullDeleteEnabled; | ||||
|     } | ||||
|  | ||||
|     isVideoFulldeleteable(): boolean { | ||||
|         return this.fullDeleteable; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default new StaticInfos(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user