You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Chore: Increase strength of Settings types (#10605)
				
					
				
			This commit is contained in:
		| @@ -941,8 +941,10 @@ packages/lib/models/Tag.test.js | ||||
| packages/lib/models/Tag.js | ||||
| packages/lib/models/dateTimeFormats.test.js | ||||
| packages/lib/models/settings/FileHandler.js | ||||
| packages/lib/models/settings/builtInMetadata.js | ||||
| packages/lib/models/settings/settingValidations.test.js | ||||
| packages/lib/models/settings/settingValidations.js | ||||
| packages/lib/models/settings/types.js | ||||
| packages/lib/models/utils/getCollator.js | ||||
| packages/lib/models/utils/getConflictFolderId.js | ||||
| packages/lib/models/utils/isItemId.js | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -920,8 +920,10 @@ packages/lib/models/Tag.test.js | ||||
| packages/lib/models/Tag.js | ||||
| packages/lib/models/dateTimeFormats.test.js | ||||
| packages/lib/models/settings/FileHandler.js | ||||
| packages/lib/models/settings/builtInMetadata.js | ||||
| packages/lib/models/settings/settingValidations.test.js | ||||
| packages/lib/models/settings/settingValidations.js | ||||
| packages/lib/models/settings/types.js | ||||
| packages/lib/models/utils/getCollator.js | ||||
| packages/lib/models/utils/getConflictFolderId.js | ||||
| packages/lib/models/utils/isItemId.js | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Setting, { SettingStorage } from '@joplin/lib/models/Setting'; | ||||
| import Setting, { AppType, SettingStorage } from '@joplin/lib/models/Setting'; | ||||
| import { SettingItemType } from '@joplin/lib/services/plugins/api/types'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
|  | ||||
| @@ -61,7 +61,7 @@ class Command extends BaseCommand { | ||||
|  | ||||
| 			const description: string[] = []; | ||||
| 			if (md.label && md.label()) description.push(md.label()); | ||||
| 			if (md.description && md.description('desktop')) description.push(md.description('desktop')); | ||||
| 			if (md.description && md.description(AppType.Desktop)) description.push(md.description(AppType.Desktop)); | ||||
|  | ||||
| 			if (description.length) props.description = description.join('. '); | ||||
| 			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
|   | ||||
| @@ -405,7 +405,7 @@ class ConfigScreenComponent extends React.Component<any, any> { | ||||
| 			return ( | ||||
| 				<div key={key} style={rowStyle}> | ||||
| 					{label} | ||||
| 					{this.renderDescription(this.props.themeId, md.description ? md.description() : null)} | ||||
| 					{this.renderDescription(this.props.themeId, md.description ? md.description(AppType.Desktop) : null)} | ||||
| 					<SettingComponent | ||||
| 						metadata={md} | ||||
| 						value={value} | ||||
| @@ -667,7 +667,7 @@ class ConfigScreenComponent extends React.Component<any, any> { | ||||
| 			}; | ||||
|  | ||||
| 			const label = [md.label()]; | ||||
| 			if (md.unitLabel) label.push(`(${md.unitLabel()})`); | ||||
| 			if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`); | ||||
|  | ||||
| 			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 			const inputStyle: any = { ...textInputBaseStyle }; | ||||
|   | ||||
| @@ -713,7 +713,7 @@ function useMenu(props: Props) { | ||||
| 					label: layoutButtonSequenceOptions[value], | ||||
| 					type: 'checkbox', | ||||
| 					click: () => { | ||||
| 						Setting.setValue('layoutButtonSequence', value); | ||||
| 						Setting.setValue('layoutButtonSequence', Number(value)); | ||||
| 					}, | ||||
| 				}); | ||||
| 			} | ||||
|   | ||||
| @@ -60,7 +60,8 @@ export const setNotesSortOrder = (field?: string, reverse?: boolean) => { | ||||
| 		nextReverse = !!nextReverse; | ||||
| 		if (perFieldReverse[nextField] !== nextReverse) { | ||||
| 			perFieldReverse[nextField] = nextReverse; | ||||
| 			Setting.setValue('notes.perFieldReverse', { ...perFieldReverse }); | ||||
| 			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied | ||||
| 			Setting.setValue('notes.perFieldReverse', { ...perFieldReverse } as any); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| import Setting, { AppType } from '@joplin/lib/models/Setting'; | ||||
| import bridge from '../bridge'; | ||||
| import processStartFlags from '@joplin/lib/utils/processStartFlags'; | ||||
| import { safeModeFlagFilename } from '@joplin/lib/BaseApplication'; | ||||
| @@ -13,7 +13,7 @@ const restartInSafeModeFromMain = async () => { | ||||
| 	// a large amount of other code) to the database. | ||||
| 	const appName = bridge().appName(); | ||||
| 	Setting.setConstant('appId', `net.cozic.${appName}`); | ||||
| 	Setting.setConstant('appType', 'desktop'); | ||||
| 	Setting.setConstant('appType', AppType.Desktop); | ||||
| 	Setting.setConstant('appName', appName); | ||||
|  | ||||
| 	// Load just enough for us to write a file in the profile directory | ||||
|   | ||||
| @@ -50,7 +50,7 @@ interface Props { | ||||
| } | ||||
|  | ||||
| function fontFamilyFromSettings() { | ||||
| 	const font = editorFont(Setting.value('style.editor.fontFamily')); | ||||
| 	const font = editorFont(Setting.value('style.editor.fontFamily') as number); | ||||
| 	return font ? `${font}, sans-serif` : 'sans-serif'; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -475,7 +475,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi | ||||
| 			} | ||||
|  | ||||
| 			const settingComp = this.settingToComponent(md.key, settings[md.key]); | ||||
| 			const relatedText = [md.label?.() ?? '', md.description?.() ?? '']; | ||||
| 			const relatedText = [md.label?.() ?? '', md.description?.(AppType.Mobile) ?? '']; | ||||
| 			addSettingComponent( | ||||
| 				settingComp, | ||||
| 				relatedText, | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import * as React from 'react'; | ||||
|  | ||||
| import { UpdateSettingValueCallback } from './types'; | ||||
| import { View, Text, TextInput } from 'react-native'; | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| import Setting, { AppType } from '@joplin/lib/models/Setting'; | ||||
| import Dropdown from '../../Dropdown'; | ||||
| import { ConfigScreenStyles } from './configScreenStyles'; | ||||
| import Slider from '@react-native-community/slider'; | ||||
| @@ -32,7 +32,7 @@ const SettingComponent: React.FunctionComponent<Props> = props => { | ||||
| 	const output: any = null; | ||||
|  | ||||
| 	const md = Setting.settingMetadata(props.settingId); | ||||
| 	const settingDescription = md.description ? md.description() : ''; | ||||
| 	const settingDescription = md.description ? md.description(AppType.Mobile) : ''; | ||||
|  | ||||
| 	const styleSheet = props.styles.styleSheet; | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService'; | ||||
| import KvStore from '@joplin/lib/services/KvStore'; | ||||
| import NoteScreen from './components/screens/Note'; | ||||
| import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen'; | ||||
| import Setting, { Env } from '@joplin/lib/models/Setting'; | ||||
| import Setting, { AppType, Env } from '@joplin/lib/models/Setting'; | ||||
| import PoorManIntervals from '@joplin/lib/PoorManIntervals'; | ||||
| import reducer, { NotesParent, parseNotesParent, serializeNotesParent } from '@joplin/lib/reducer'; | ||||
| import ShareExtension from './utils/ShareExtension'; | ||||
| @@ -505,9 +505,9 @@ async function initialize(dispatch: Function) { | ||||
| 		value: profileConfig, | ||||
| 	}); | ||||
|  | ||||
| 	Setting.setConstant('env', __DEV__ ? 'dev' : 'prod'); | ||||
| 	Setting.setConstant('env', __DEV__ ? Env.Dev : Env.Prod); | ||||
| 	Setting.setConstant('appId', 'net.cozic.joplin-mobile'); | ||||
| 	Setting.setConstant('appType', 'mobile'); | ||||
| 	Setting.setConstant('appType', AppType.Mobile); | ||||
| 	Setting.setConstant('tempDir', await initializeTempDir()); | ||||
| 	Setting.setConstant('cacheDir', `${getProfilesRootDir()}/cache`); | ||||
| 	const resourceDir = getResourceDir(currentProfile, isSubProfile); | ||||
| @@ -619,7 +619,7 @@ async function initialize(dispatch: Function) { | ||||
| 			reg.logger().info(`First start: detected locale as ${detectedLocale}`); | ||||
|  | ||||
| 			Setting.skipDefaultMigrations(); | ||||
| 			Setting.setValue('firstStart', 0); | ||||
| 			Setting.setValue('firstStart', false); | ||||
| 		} else { | ||||
| 			Setting.applyDefaultMigrations(); | ||||
| 		} | ||||
| @@ -788,7 +788,7 @@ async function initialize(dispatch: Function) { | ||||
| 	const pluginSettings = pluginService.unserializePluginSettings(Setting.value('plugins.states')); | ||||
|  | ||||
| 	const updatedSettings = pluginService.clearUpdateState(pluginSettings); | ||||
| 	Setting.setValue('plugins.states', pluginService.serializePluginSettings(updatedSettings)); | ||||
| 	Setting.setValue('plugins.states', updatedSettings); | ||||
|  | ||||
| 	// ---------------------------------------------------------------------------- | ||||
| 	// Keep this below to test react-native-rsa-native | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|   "packageManager": "yarn@3.6.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "buildPluginDoc_": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/" | ||||
|      "buildPluginDoc_": "typedoc --exclude '../lib/models/**' --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "typedoc": "0.17.8", | ||||
|   | ||||
| @@ -687,7 +687,7 @@ export default class BaseApplication { | ||||
| 		const tempDir = `${profileDir}/tmp`; | ||||
| 		const cacheDir = `${profileDir}/cache`; | ||||
|  | ||||
| 		Setting.setConstant('env', initArgs.env); | ||||
| 		Setting.setConstant('env', initArgs.env as Env); | ||||
| 		Setting.setConstant('resourceDirName', resourceDirName); | ||||
| 		Setting.setConstant('resourceDir', resourceDir); | ||||
| 		Setting.setConstant('tempDir', tempDir); | ||||
| @@ -789,12 +789,12 @@ export default class BaseApplication { | ||||
| 			Setting.skipDefaultMigrations(); | ||||
|  | ||||
| 			if (Setting.value('env') === 'dev') { | ||||
| 				Setting.setValue('showTrayIcon', 0); | ||||
| 				Setting.setValue('autoUpdateEnabled', 0); | ||||
| 				Setting.setValue('showTrayIcon', false); | ||||
| 				Setting.setValue('autoUpdateEnabled', false); | ||||
| 				Setting.setValue('sync.interval', 3600); | ||||
| 			} | ||||
|  | ||||
| 			Setting.setValue('firstStart', 0); | ||||
| 			Setting.setValue('firstStart', false); | ||||
| 		} else { | ||||
| 			Setting.applyDefaultMigrations(); | ||||
| 			Setting.applyUserSettingMigration(); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import Logger from '@joplin/utils/Logger'; | ||||
| import { defaultProfileConfig } from '../services/profileConfig/types'; | ||||
| import { createNewProfile, saveProfileConfig } from '../services/profileConfig'; | ||||
| import initProfile from '../services/profileConfig/initProfile'; | ||||
| import { defaultPluginSetting } from '../services/plugins/PluginService'; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| async function loadSettingsFromFile(): Promise<any> { | ||||
| @@ -206,7 +207,7 @@ describe('models/Setting', () => { | ||||
| 	it('should save and load settings from file', (async () => { | ||||
| 		Setting.setValue('sync.target', 9); // Saved to file | ||||
| 		Setting.setValue('encryption.passwordCache', {}); // Saved to keychain or db | ||||
| 		Setting.setValue('plugins.states', { test: true }); // Always saved to db | ||||
| 		Setting.setValue('plugins.states', { test: defaultPluginSetting() }); // Always saved to db | ||||
| 		await Setting.saveAll(); | ||||
|  | ||||
| 		{ | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1573
									
								
								packages/lib/models/settings/builtInMetadata.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1573
									
								
								packages/lib/models/settings/builtInMetadata.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										108
									
								
								packages/lib/models/settings/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								packages/lib/models/settings/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| import type { BuiltInMetadataValues } from './builtInMetadata'; | ||||
|  | ||||
| export enum SettingItemType { | ||||
| 	Int = 1, | ||||
| 	String = 2, | ||||
| 	Bool = 3, | ||||
| 	Array = 4, | ||||
| 	Object = 5, | ||||
| 	Button = 6, | ||||
| } | ||||
|  | ||||
| export enum SettingItemSubType { | ||||
| 	FilePathAndArgs = 'file_path_and_args', | ||||
| 	FilePath = 'file_path', // Not supported on mobile! | ||||
| 	DirectoryPath = 'directory_path', // Not supported on mobile! | ||||
| 	FontFamily = 'font_family', | ||||
| 	MonospaceFontFamily = 'monospace_font_family', | ||||
| } | ||||
|  | ||||
| export enum SettingStorage { | ||||
| 	Database = 1, | ||||
| 	File = 2, | ||||
| } | ||||
|  | ||||
|  | ||||
| // This is the definition of a setting item | ||||
| export interface SettingItem { | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	value: any; | ||||
| 	type: SettingItemType; | ||||
| 	public: boolean; | ||||
|  | ||||
| 	subType?: string; | ||||
| 	key?: string; | ||||
| 	isEnum?: boolean; | ||||
| 	section?: string; | ||||
| 	label?(): string; | ||||
| 	description?: (_appType: AppType)=> string; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	options?(): any; | ||||
| 	optionsOrder?(): string[]; | ||||
| 	appTypes?: AppType[]; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	show?(settings: any): boolean; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	filter?(value: any): any; | ||||
| 	secure?: boolean; | ||||
| 	advanced?: boolean; | ||||
| 	minimum?: number; | ||||
| 	maximum?: number; | ||||
| 	step?: number; | ||||
| 	onClick?(): void; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code before rule was applied | ||||
| 	unitLabel?: (value: any)=> string; | ||||
| 	needRestart?: boolean; | ||||
| 	autoSave?: boolean; | ||||
| 	storage?: SettingStorage; | ||||
| 	hideLabel?: boolean; | ||||
|  | ||||
| 	// In a multi-profile context, all settings are by default local - they take | ||||
| 	// their value from the current profile. This flag can be set to specify | ||||
| 	// that the setting is global and that its value should come from the root | ||||
| 	// profile. This flag only applies to sub-profiles. | ||||
| 	// | ||||
| 	// At the moment, all global settings must be saved to file (have the | ||||
| 	// storage attribute set to "file") because it's simpler to load the root | ||||
| 	// profile settings.json than load the whole SQLite database. This | ||||
| 	// restriction is not an issue normally since all settings that are | ||||
| 	// considered global are also the user-facing ones. | ||||
| 	isGlobal?: boolean; | ||||
| } | ||||
|  | ||||
| export interface SettingItems { | ||||
| 	[key: string]: SettingItem; | ||||
| } | ||||
|  | ||||
| export enum SettingSectionSource { | ||||
| 	Default = 1, | ||||
| 	Plugin = 2, | ||||
| } | ||||
|  | ||||
| export interface SettingSection { | ||||
| 	label: string; | ||||
| 	iconName?: string; | ||||
| 	description?: string; | ||||
| 	name?: string; | ||||
| 	source?: SettingSectionSource; | ||||
| } | ||||
|  | ||||
| export enum SyncStartupOperation { | ||||
| 	None = 0, | ||||
| 	ClearLocalSyncState = 1, | ||||
| 	ClearLocalData = 2, | ||||
| } | ||||
|  | ||||
| export enum Env { | ||||
| 	Undefined = 'SET_ME', | ||||
| 	Dev = 'dev', | ||||
| 	Prod = 'prod', | ||||
| } | ||||
|  | ||||
| export enum AppType { | ||||
| 	Desktop = 'desktop', | ||||
| 	Mobile = 'mobile', | ||||
| 	Cli = 'cli', | ||||
| } | ||||
|  | ||||
| export type SettingsRecord = BuiltInMetadataValues & { [key: string]: unknown }; | ||||
| @@ -12,6 +12,7 @@ import { getListRendererIds } from './services/noteList/renderers'; | ||||
| import { ProcessResultsRow } from './services/search/SearchEngine'; | ||||
| import { getDisplayParentId } from './services/trash'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { SettingsRecord } from './models/settings/types'; | ||||
| const fastDeepEqual = require('fast-deep-equal'); | ||||
| const { ALL_NOTES_FILTER_ID } = require('./reserved-ids'); | ||||
| const { createSelectorCreator, defaultMemoize } = require('reselect'); | ||||
| @@ -100,8 +101,7 @@ export interface State { | ||||
| 	syncReport: any; | ||||
| 	searchQuery: string; | ||||
| 	searchResults: ProcessResultsRow[]; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	settings: Record<string, any>; | ||||
| 	settings: Partial<SettingsRecord>; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	sharedData: any; | ||||
| 	appState: string; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Setting from './models/Setting'; | ||||
| import Setting, { Env } from './models/Setting'; | ||||
| import { reg } from './registry'; | ||||
|  | ||||
| const sync = { | ||||
| @@ -9,7 +9,7 @@ describe('Registry', () => { | ||||
| 	let originalSyncTarget: typeof reg.syncTarget; | ||||
|  | ||||
| 	beforeAll(() => { | ||||
| 		Setting.setConstant('env', 'prod'); | ||||
| 		Setting.setConstant('env', Env.Prod); | ||||
| 		originalSyncTarget = reg.syncTarget; | ||||
| 		reg.syncTarget = () => ({ | ||||
| 			isAuthenticated: () => true, | ||||
| @@ -18,7 +18,7 @@ describe('Registry', () => { | ||||
| 	}); | ||||
|  | ||||
| 	afterAll(() => { | ||||
| 		Setting.setConstant('env', 'dev'); | ||||
| 		Setting.setConstant('env', Env.Dev); | ||||
| 		reg.syncTarget = originalSyncTarget; | ||||
| 	}); | ||||
|  | ||||
|   | ||||
| @@ -215,8 +215,8 @@ export default class PluginService extends BaseService { | ||||
| 		return output as PluginSettings; | ||||
| 	} | ||||
|  | ||||
| 	public serializePluginSettings(settings: PluginSettings): string { | ||||
| 		return JSON.stringify(settings); | ||||
| 	public serializePluginSettings(settings: PluginSettings) { | ||||
| 		return settings; | ||||
| 	} | ||||
|  | ||||
| 	public pluginIdByContentScriptId(contentScriptId: string): string { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| import BaseApplication from '../BaseApplication'; | ||||
| import BaseModel from '../BaseModel'; | ||||
| import Logger, { TargetType, LoggerWrapper, LogLevel } from '@joplin/utils/Logger'; | ||||
| import Setting from '../models/Setting'; | ||||
| import Setting, { AppType, Env } from '../models/Setting'; | ||||
| import BaseService from '../services/BaseService'; | ||||
| import FsDriverNode from '../fs-driver-node'; | ||||
| import time from '../time'; | ||||
| @@ -190,14 +190,14 @@ BaseItem.loadClass('MasterKey', MasterKey); | ||||
| BaseItem.loadClass('Revision', Revision); | ||||
|  | ||||
| Setting.setConstant('appId', 'net.cozic.joplintest-cli'); | ||||
| Setting.setConstant('appType', 'cli'); | ||||
| Setting.setConstant('appType', AppType.Cli); | ||||
| Setting.setConstant('tempDir', baseTempDir); | ||||
| Setting.setConstant('cacheDir', baseTempDir); | ||||
| Setting.setConstant('resourceDir', baseTempDir); | ||||
| Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`); | ||||
| Setting.setConstant('profileDir', profileDir); | ||||
| Setting.setConstant('rootProfileDir', rootProfileDir); | ||||
| Setting.setConstant('env', 'dev'); | ||||
| Setting.setConstant('env', Env.Dev); | ||||
|  | ||||
| BaseService.logger_ = logger; | ||||
|  | ||||
| @@ -640,7 +640,7 @@ async function initFileApi() { | ||||
| 		mustRunInBand(); | ||||
|  | ||||
| 		const { parameters, setEnvOverride } = require('../parameters.js'); | ||||
| 		Setting.setConstant('env', 'dev'); | ||||
| 		Setting.setConstant('env', Env.Dev); | ||||
| 		setEnvOverride('test'); | ||||
| 		const config = parameters().oneDriveTest; | ||||
| 		const api = new OneDriveApi(config.id, config.secret, false); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user