diff --git a/docs/3-using-maintainerr/1-rules/Glossary.md b/docs/3-using-maintainerr/1-rules/Glossary.md new file mode 100644 index 00000000..1e310e5f --- /dev/null +++ b/docs/3-using-maintainerr/1-rules/Glossary.md @@ -0,0 +1,462 @@ +## Rule glossary + +This glossary describes the available rules that can be used in the maintainerr. +Each rule contains the media type it applies to, the key and the data type of the returned value. + +The key is used for identification in Yaml rule files. + +### Plex + +#### Date added + +> The date when the Plex item was added to the server. + +- Key: Plex.addDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Viewed by (username) + +> List of Plex usernames who have viewed the Plex item. + +- Key: Plex.seenBy +- Availability: movies, shows, seasons, episodes +- Type: text[] + +#### Release date + +> The release date of the Plex item. + +- Key: Plex.releaseDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### User rating (scale 1-10) + +> The user rating of the Plex item on a scale of 1 to 10. + +- Key: Plex.rating_user +- Availability: movies, shows, seasons, episodes +- Type: number + +#### People involved + +> List of people involved in the Plex item. This includes actors, directors,.. + +- Key: Plex.people +- Availability: movies, shows, seasons, episodes +- Type: text[] + +#### Times viewed + +> The number of times the Plex item has been viewed. + +- Key: Plex.viewCount +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Present in amount of other collections + +> The number of collections the Plex item is present in. + +- Key: Plex.collections +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Last view date + +> The date when the Plex item was last viewed. + +- Key: Plex.lastViewedAt +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Media file resolution (4k, 1080,..) + +> The resolutions of the media file associated with the Plex item. Possibilities include 4k, 1080, 720, 480, 360, 240. + +- Key: Plex.fileVideoResolution +- Availability: movies, shows, seasons, episodes +- Type: text + +#### Media file bitrate + +> The bitrate of the media file associated with the Plex item. + +- Key: Plex.fileBitrate +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Media file codec + +> The codec of the media file associated with the Plex item. + +- Key: Plex.fileVideoCodec +- Availability: movies, shows, seasons, episodes +- Type: text + +#### List of genres (Action, Adventure,..) + +> List of genres associated with the Plex item. + +- Key: Plex.genre +- Availability: movies, shows, seasons, episodes +- Type: text[] + +#### Users that saw all available episodes + +> List of users who have seen all available episodes of the Plex item. This rule is only available for shows. + +- Key: Plex.sw_allEpisodesSeenBy +- Availability: shows, seasons +- Type: text[] + +#### Newest episode view date + +> The date when the newest episode of the Plex item was viewed. This rule is only available for shows. + +- Key: Plex.sw_lastWatched +- Availability: shows, seasons +- Type: date + +#### Amount of available episodes + +> The total number of episodes available for the Plex item. This rule is only available for shows. + +- Key: Plex.sw_episodes +- Availability: shows, seasons +- Type: number + +#### Amount of watched episodes + +> The number of episodes that have been watched for the Plex item. This rule is only available for shows. + +- Key: Plex.sw_viewedEpisodes +- Availability: shows, seasons +- Type: number + +#### Last episode added at + +> The date when the last episode was added to the Plex item. This rule is only available for shows. + +- Key: Plex.sw_lastEpisodeAddedAt +- Availability: shows, seasons +- Type: date + +#### Total views + +> The total number of views for the Plex item. This rule is only available for shows. + +- Key: Plex.sw_amountOfViews +- Availability: shows, seasons, episodes +- Type: number + +#### Users that watch the show/season/episode + +> List of users who watch the Plex item. This rule is only available for shows. + +- Key: Plex.sw_watchers +- Availability: shows, seasons, episodes +- Type: text[] + +#### Collections media is present in (titles) + +> List of collections that the Plex item is present in. + +- Key: Plex.collection_names +- Availability: movies, shows, seasons, episodes +- Type: text[] + +#### Present in amount of playlists + +> The number of playlists the Plex item is present in. + +- Key: Plex.playlists +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Playlists media is present in (titles) + +> List of playlists that the Plex item is present in. + +- Key: Plex.playlist_names +- Availability: movies, shows, seasons, episodes +- Type: text[] + +#### Critics rating (scale 1-10) + +> The critics rating of the Plex item on a scale of 1 to 10. This will mostly include the rotten tomatoes critics rating. + +- Key: Plex.rating_critics +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Audience rating (scale 1-10) + +> The audience rating of the Plex item on a scale of 1 to 10. This wil include the rotten tomatoes audience rating, or the imdb, tvdb, tmdb,.. rating. Depends on your server configuration. + +- Key: Plex.rating_audience +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Labels + +> List of labels associated with the Plex item. + +- Key: Plex.labels +- Availability: movies, shows, seasons, episodes +- Type: text[] + +### Radarr + +#### Date added + +> The date when the Radarr item was added. + +- Key: Radarr.addDate +- Availability: movies +- Type: date + +#### Tags (Text if 1, otherwise list) + +> List of tags associated with the Radarr item. + +- Key: Radarr.tags +- Availability: movies +- Type: text[] + +#### Quality profile + +> The quality profile of the Radarr item. + +- Key: Radarr.profile +- Availability: movies +- Type: text + +#### Release date + +> The release date of the Radarr item. + +- Key: Radarr.releaseDate +- Availability: movies +- Type: date + +#### Is monitored + +> Indicates whether the Radarr item is monitored. + +- Key: Radarr.monitored +- Availability: movies +- Type: boolean + +#### In cinemas date + +> The date when the Radarr item was released in cinemas. + +- Key: Radarr.inCinemas +- Availability: movies +- Type: date + +#### File - size in MB + +> The size of the file associated with the Radarr item in megabytes. + +- Key: Radarr.fileSize +- Availability: movies +- Type: number + +#### File - audio channels + +> List of audio channels of the file associated with the Radarr item. + +- Key: Radarr.fileAudioChannels +- Availability: movies +- Type: number[] + +#### File - quality (2160, 1080,..) + +> List of quality levels of the file associated with the Radarr item. + +- Key: Radarr.fileQuality +- Availability: movies +- Type: number[] + +#### File - download date + +> The date when the file associated with the Radarr item was downloaded. + +- Key: Radarr.fileDate +- Availability: movies +- Type: date + +#### File - runtime in minutes + +> The runtime of the file associated with the Radarr item in minutes. + +- Key: Radarr.runTime +- Availability: movies +- Type: number + +### Sonarr + +#### Date added + +> The date when the Sonarr item was added. + +- Key: Sonarr.addDate +- Availability: shows +- Type: date + +#### Files - Disk size in MB + +> The disk size of the entire show, season or episode in megabytes. + +- Key: Sonarr.diskSizeEntireShow +- Availability: shows, seasons, episodes +- Type: number + +#### Tags (show) + +> List of tags associated with the Sonarr item. + +- Key: Sonarr.tags +- Availability: shows, seasons, episodes +- Type: text[] + +#### Quality profile ID + +> The quality profile ID of the Sonarr item. + +- Key: Sonarr.qualityProfileId +- Availability: shows, seasons, episodes +- Type: number + +#### First air date + +> The first air date of the Sonarr item. + +- Key: Sonarr.firstAirDate +- Availability: shows, seasons, episodes +- Type: date + +#### Number of seasons / episodes (also unavailable) + +> The number of seasons or episodes for the Sonarr item. This will also count the unavailable episodes. + +- Key: Sonarr.seasons +- Availability: shows, seasons +- Type: number + +#### Status (continuing, ended) + +> The status of the Sonarr item. + +- Key: Sonarr.status +- Availability: shows +- Type: text + +#### Show ended + +> Indicates whether the Sonarr show has ended. + +- Key: Sonarr.ended +- Availability: shows +- Type: boolean + +#### Is monitored + +> Indicates whether the Sonarr item is monitored. + +- Key: Sonarr.monitored +- Availability: shows, seasons, episodes +- Type: boolean + +#### Has unaired episodes + +> Indicates whether the Sonarr show/season has unaired episodes. + +- Key: Sonarr.unaired_episodes +- Availability: shows, seasons, episodes +- Type: boolean + +#### Number of monitored seasons / episodes + +> The number of monitored seasons or episodes for the Sonarr item. + +- Key: Sonarr.seasons_monitored +- Availability: shows, seasons +- Type: number + +#### Season has unaired episodes + +> Indicates whether the Sonarr season has unaired episodes. + +- Key: Sonarr.unaired_episodes_season +- Availability: episodes +- Type: boolean + +#### Is (part of) latest aired/airing season + +> Indicates whether the Sonarr item is part of the latest aired or airing season. + +- Key: Sonarr.part_of_latest_season +- Availability: seasons, episodes +- Type: boolean + +### Overseerr + +#### Requested by user (Plex username) + +> The username of the Plex user who requested the media in Overseerr. + +- Key: Overseerr.addUser +- Availability: movies, shows, seasons, episodes +- Type: text + +#### Request date + +> The date when the media was requested in Overseerr. + +- Key: Overseerr.requestDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Release/air date + +> The release or air date of the media in Overseerr. + +- Key: Overseerr.releaseDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Approval date + +> The date when the media request was approved in Overseerr. + +- Key: Overseerr.approvalDate +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Media downloaded date + +> The date when the media was downloaded in Overseerr. + +- Key: Overseerr.mediaAddedAt +- Availability: movies, shows, seasons, episodes +- Type: date + +#### Amount of requests + +> The total number of requests for the media in Overseerr. + +- Key: Overseerr.amountRequested +- Availability: movies, shows, seasons, episodes +- Type: number + +#### Requested in Overseerr + +> Indicates whether the media was requested in Overseerr. + +- Key: Overseerr.isRequested +- Availability: movies, shows, seasons, episodes +- Type: boolean diff --git a/package.json b/package.json index 5bcf2b4e..8ddc93ac 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "migration:generate": "ts-node node_modules/typeorm/cli.js migration:generate --dataSource ./datasource-config.ts -p" }, "dependencies": { + "@headlessui/react": "1.7.12", "@heroicons/react": "^1.0.6", + "@monaco-editor/react": "^4.6.0", "@nestjs/common": "^10.2.7", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "^10.3.0", @@ -50,6 +52,7 @@ "plex-api": "^5.3.2", "react": "18.2.0", "react-dom": "18.2.0", + "react-toast-notifications": "^2.5.1", "react-transition-group": "^4.4.5", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.5", @@ -58,7 +61,8 @@ "swr": "^2.2.4", "typeorm": "^0.3.17", "web-push": "^3.6.6", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "yaml": "^2.3.4" }, "devDependencies": { "@automock/jest": "^1.4.0", @@ -94,6 +98,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-config-next": "14.0.4", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.7.0", "jsdoc": "^4.0.2", diff --git a/server/src/modules/api/plex-api/enums/plex-data-type-enum.ts b/server/src/modules/api/plex-api/enums/plex-data-type-enum.ts index eda41b5d..684fd6d7 100644 --- a/server/src/modules/api/plex-api/enums/plex-data-type-enum.ts +++ b/server/src/modules/api/plex-api/enums/plex-data-type-enum.ts @@ -6,3 +6,11 @@ export enum EPlexDataType { SEASONS = 3, EPISODES = 4, } + +// EPlexDataType values as strings +export const PlexDataTypeStrings: string[] = [ + 'MOVIES', + 'SHOWS', + 'SEASONS', + 'EPISODES', +]; diff --git a/server/src/modules/rules/constants/rules.constants.ts b/server/src/modules/rules/constants/rules.constants.ts index ea1da2e0..ccd1b3da 100644 --- a/server/src/modules/rules/constants/rules.constants.ts +++ b/server/src/modules/rules/constants/rules.constants.ts @@ -1,6 +1,6 @@ import { EPlexDataType } from '../../api/plex-api/enums/plex-data-type-enum'; -export const enum RulePossibility { +export enum RulePossibility { BIGGER, SMALLER, EQUALS, @@ -15,7 +15,7 @@ export const enum RulePossibility { NOT_CONTAINS_PARTIAL, } -export const enum RuleOperators { +export enum RuleOperators { AND, OR, } @@ -41,37 +41,51 @@ export const enum MediaType { } export class RuleType { - static readonly NUMBER = new RuleType('0', [ - RulePossibility.BIGGER, - RulePossibility.SMALLER, - RulePossibility.EQUALS, - RulePossibility.NOT_EQUALS, - RulePossibility.CONTAINS, - RulePossibility.NOT_CONTAINS, - ]); - static readonly DATE = new RuleType('1', [ - RulePossibility.EQUALS, - RulePossibility.NOT_EQUALS, - RulePossibility.BEFORE, - RulePossibility.AFTER, - RulePossibility.IN_LAST, - RulePossibility.IN_NEXT, - ]); - static readonly TEXT = new RuleType('2', [ - RulePossibility.EQUALS, - RulePossibility.NOT_EQUALS, - RulePossibility.CONTAINS, - RulePossibility.NOT_CONTAINS, - RulePossibility.CONTAINS_PARTIAL, - RulePossibility.NOT_CONTAINS_PARTIAL, - ]); - static readonly BOOL = new RuleType('3', [ - RulePossibility.EQUALS, - RulePossibility.NOT_EQUALS, - ]); - private constructor( + static readonly NUMBER = new RuleType( + '0', + [ + RulePossibility.BIGGER, + RulePossibility.SMALLER, + RulePossibility.EQUALS, + RulePossibility.NOT_EQUALS, + RulePossibility.CONTAINS, + RulePossibility.NOT_CONTAINS, + ], + 'number', + ); + static readonly DATE = new RuleType( + '1', + [ + RulePossibility.EQUALS, + RulePossibility.NOT_EQUALS, + RulePossibility.BEFORE, + RulePossibility.AFTER, + RulePossibility.IN_LAST, + RulePossibility.IN_NEXT, + ], + 'date', + ); + static readonly TEXT = new RuleType( + '2', + [ + RulePossibility.EQUALS, + RulePossibility.NOT_EQUALS, + RulePossibility.CONTAINS, + RulePossibility.NOT_CONTAINS, + RulePossibility.CONTAINS_PARTIAL, + RulePossibility.NOT_CONTAINS_PARTIAL, + ], + 'text', + ); + static readonly BOOL = new RuleType( + '3', + [RulePossibility.EQUALS, RulePossibility.NOT_EQUALS], + 'boolean', + ); + public constructor( private readonly key: string, public readonly possibilities: number[], + public readonly humanName: string, ) {} toString() { return this.key; @@ -300,13 +314,7 @@ export class RuleConstants { mediaType: MediaType.MOVIE, type: RuleType.DATE, } as Property, - { - id: 1, - name: 'fileDate', - humanName: 'Date file downloaded', - mediaType: MediaType.MOVIE, - type: RuleType.DATE, - } as Property, + // Don't use ID 1, It was once used for an old rule value. Changing the id's messes up existing rules. { id: 2, name: 'tags', diff --git a/server/src/modules/rules/dtos/rule.dto.ts b/server/src/modules/rules/dtos/rule.dto.ts index 4e91f0b2..35383928 100644 --- a/server/src/modules/rules/dtos/rule.dto.ts +++ b/server/src/modules/rules/dtos/rule.dto.ts @@ -6,4 +6,5 @@ export class RuleDto { firstVal: [number, number]; lastVal?: [number, number]; customVal?: { ruleTypeId: number; value: string }; + section: number; } diff --git a/server/src/modules/rules/helpers/yaml.service.ts b/server/src/modules/rules/helpers/yaml.service.ts new file mode 100644 index 00000000..838ee4c0 --- /dev/null +++ b/server/src/modules/rules/helpers/yaml.service.ts @@ -0,0 +1,263 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { RuleDto } from '../dtos/rule.dto'; +import { ReturnStatus } from '../rules.service'; +import { + RuleConstants, + RuleOperators, + RulePossibility, + RuleType, +} from '../constants/rules.constants'; +import YAML from 'yaml'; +import { + EPlexDataType, + PlexDataTypeStrings, +} from '../../..//modules/api/plex-api/enums/plex-data-type-enum'; + +interface IRuleYamlParent { + mediaType: string; + rules: ISectionYaml[]; +} + +interface ISectionYaml { + [key: number]: IRuleYaml[]; +} + +interface ICustomYamlValue { + type: string; + value: string | number; +} + +interface IRuleYaml { + operator?: string; + action: string; + firstValue: string; + lastValue?: string; + customValue?: ICustomYamlValue; +} + +@Injectable() +export class RuleYamlService { + private readonly logger = new Logger(RuleYamlService.name); + + ruleConstants: RuleConstants; + constructor() { + this.ruleConstants = new RuleConstants(); + } + public encode(rules: RuleDto[], mediaType: number): ReturnStatus { + try { + let workingSection = { id: 0, rules: [] }; + const sections: ISectionYaml[] = []; + + for (const rule of rules) { + if (rule.section !== workingSection.id) { + // push section and prepare next section + sections.push({ + [+workingSection.id]: workingSection.rules, + }); + workingSection = { id: rule.section, rules: [] }; + } + + // transform rule and add to workingSection + workingSection.rules.push({ + ...(rule.operator ? { operator: RuleOperators[+rule.operator] } : {}), + firstValue: this.getValueIdentifier(rule.firstVal), + action: RulePossibility[+rule.action], + ...(rule.lastVal + ? { lastValue: this.getValueIdentifier(rule.lastVal) } + : {}), + ...(rule.customVal + ? { customValue: this.getCustomValueIdentifier(rule.customVal) } + : {}), + }); + } + + // push last workingsection to sections + sections.push({ [+workingSection.id]: workingSection.rules }); + const fullObject: IRuleYamlParent = { + mediaType: PlexDataTypeStrings[+mediaType - 1], + rules: sections, + }; + // Transform to yaml + const yaml = YAML.stringify(fullObject); + + return { + code: 1, + result: yaml, + message: 'success', + }; + } catch (e) { + this.logger.warn(`Yaml export failed : ${e.message}`); + this.logger.debug(e); + return { + code: 0, + message: 'Yaml export failed. Please check logs', + }; + } + } + + public decode(yaml: string, mediaType: number): ReturnStatus { + try { + const decoded: IRuleYamlParent = YAML.parse(yaml); + const rules: RuleDto[] = []; + let idRef = 0; + + // Break when media types are incompatible + if (+mediaType !== +EPlexDataType[decoded.mediaType.toUpperCase()]) { + this.logger.warn(`Yaml import failed. Incompatible media types`); + this.logger.debug( + `media type with ID ${+mediaType} is not compatible with media type with ID ${ + EPlexDataType[decoded.mediaType.toUpperCase()] + } `, + ); + + return { + code: 0, + message: 'Yaml import failed. Incompatible media types', + }; + } + + for (const section of decoded.rules) { + for (const rule of section[idRef]) { + rules.push({ + operator: rule.operator + ? +RuleOperators[rule.operator.toUpperCase()] + : null, + action: +RulePossibility[rule.action.toUpperCase()], + section: idRef, + firstVal: this.getValueFromIdentifier( + rule.firstValue.toLowerCase(), + ), + ...(rule.lastValue + ? { + lastVal: this.getValueFromIdentifier( + rule.lastValue.toLowerCase(), + ), + } + : {}), + ...(rule.customValue + ? { + customVal: this.getCustomValueFromIdentifier( + rule.customValue, + ), + } + : {}), + }); + } + idRef++; + } + + const returnObj: { mediaType: number; rules: RuleDto[] } = { + mediaType: EPlexDataType[decoded.mediaType], + rules: rules, + }; + + return { + code: 1, + result: JSON.stringify(returnObj), + message: 'success', + }; + } catch (e) { + this.logger.warn(`Yaml import failed. Is the yaml valid?`); + this.logger.debug(e); + return { + code: 0, + message: 'Import failed, please check your yaml', + }; + } + } + private getValueIdentifier(location: [number, number]) { + const application = this.ruleConstants.applications[location[0]].name; + const rule = + this.ruleConstants.applications[location[0]].props[location[1]].name; + + return application + '.' + rule; + } + + private getValueFromIdentifier(identifier: string): [number, number] { + const application = identifier.split('.')[0]; + const rule = identifier.split('.')[1]; + + const applicationConstant = this.ruleConstants.applications.find( + (el) => el.name.toLowerCase() === application.toLowerCase(), + ); + + const ruleConstant = applicationConstant.props.find( + (el) => el.name.toLowerCase() === rule.toLowerCase(), + ); + return [applicationConstant.id, ruleConstant.id]; + } + + private getCustomValueIdentifier(customValue: { + ruleTypeId: number; + value: string; + }): ICustomYamlValue { + let ruleType: RuleType; + let value: string | number; + switch (customValue.ruleTypeId) { + case 0: + if (+customValue.value % 86400 === 0 && +customValue.value != 0) { + // when it's custom_days, translate to custom_days + ruleType = new RuleType('4', [], 'custom_days'); + value = (+customValue.value / 86400).toString(); + } else { + // otherwise, it's a normal number + ruleType = RuleType.NUMBER; + value = +customValue.value; + } + break; + case 1: + ruleType = RuleType.DATE; + value = customValue.value; + break; + case 2: + ruleType = RuleType.TEXT; + value = customValue.value; + break; + case 3: + ruleType = RuleType.BOOL; + value = customValue.value == '1' ? 'true' : 'false'; + break; + } + + return { type: ruleType.humanName, value: value }; + } + + private getCustomValueFromIdentifier(identifier: ICustomYamlValue): { + ruleTypeId: number; + value: string; + } { + let ruleType: RuleType; + let value: string; + + switch (identifier.type.toUpperCase()) { + case 'NUMBER': + ruleType = RuleType.NUMBER; + value = identifier.value.toString(); + break; + case 'DATE': + ruleType = RuleType.DATE; + value = identifier.value.toString(); + break; + case 'TEXT': + ruleType = RuleType.TEXT; + value = identifier.value.toString(); + break; + case 'BOOLEAN': + ruleType = RuleType.BOOL; + value = identifier.value == 'true' ? '1' : '0'; + break; + case 'BOOL': + ruleType = RuleType.BOOL; + value = identifier.value == 'true' ? '1' : '0'; + break; + case 'CUSTOM_DAYS': + ruleType = RuleType.NUMBER; + value = (+identifier.value * 86400).toString(); + } + + return { + ruleTypeId: +ruleType.toString(), // tostring returns the key + value: value, + }; + } +} diff --git a/server/src/modules/rules/rules.controller.ts b/server/src/modules/rules/rules.controller.ts index 898a3f6d..909c932d 100644 --- a/server/src/modules/rules/rules.controller.ts +++ b/server/src/modules/rules/rules.controller.ts @@ -13,6 +13,7 @@ import { ExclusionAction, ExclusionContextDto } from './dtos/exclusion.dto'; import { RulesDto } from './dtos/rules.dto'; import { RuleExecutorService } from './rule-executor.service'; import { ReturnStatus, RulesService } from './rules.service'; +import { RuleDto } from './dtos/rule.dto'; @Controller('api/rules') export class RulesController { @@ -132,4 +133,47 @@ export class RulesController { }; } } + + /** + * Encodes an array of RuleDto objects to YAML format. + * + * @param {RuleDto[]} rules - The array of RuleDto objects to be encoded. + * @return {Promise} A Promise that resolves to a ReturnStatus object. + */ + @Post('/yaml/encode') + async yamlEncode( + @Body() body: { rules: string; mediaType: number }, + ): Promise { + try { + return this.rulesService.encodeToYaml( + JSON.parse(body.rules), + body.mediaType, + ); + } catch (err) { + return { + code: 0, + result: 'Invalid input', + }; + } + } + + /** + * Decodes a YAML-encoded string and returns an array of RuleDto objects. + * + * @param {string} body - The YAML-encoded string to decode. + * @return {Promise} - A Promise that resolves to the decoded ReturnStatus object. + */ + @Post('/yaml/decode') + async yamlDecode( + @Body() body: { yaml: string; mediaType: number }, + ): Promise { + try { + return this.rulesService.decodeFromYaml(body.yaml, body.mediaType); + } catch (err) { + return { + code: 0, + result: 'Invalid input', + }; + } + } } diff --git a/server/src/modules/rules/rules.module.ts b/server/src/modules/rules/rules.module.ts index d8200a6d..1bb0624d 100644 --- a/server/src/modules/rules/rules.module.ts +++ b/server/src/modules/rules/rules.module.ts @@ -22,6 +22,7 @@ import { Exclusion } from './entities/exclusion.entities'; import { CommunityRuleKarma } from './entities/community-rule-karma.entities'; import { Settings } from '../settings/entities/settings.entities'; import { RuleMaintenanceService } from './rule-maintenance.service'; +import { RuleYamlService } from './helpers/yaml.service'; @Module({ imports: [ @@ -50,6 +51,7 @@ import { RuleMaintenanceService } from './rule-maintenance.service'; SonarrGetterService, OverseerrGetterService, ValueGetterService, + RuleYamlService, ], controllers: [RulesController], }) diff --git a/server/src/modules/rules/rules.service.ts b/server/src/modules/rules/rules.service.ts index ef9790ff..fabdf2b7 100644 --- a/server/src/modules/rules/rules.service.ts +++ b/server/src/modules/rules/rules.service.ts @@ -25,6 +25,7 @@ import { EPlexDataType } from '../api/plex-api/enums/plex-data-type-enum'; import { Settings } from '../settings/entities/settings.entities'; import _ from 'lodash'; import { AddCollectionMedia } from '../collections/interfaces/collection-media.interface'; +import { RuleYamlService } from './helpers/yaml.service'; export interface ReturnStatus { code: 0 | 1; @@ -57,6 +58,7 @@ export class RulesService { private readonly collectionService: CollectionsService, private readonly plexApi: PlexApiService, private readonly connection: Connection, + private readonly ruleYamlService: RuleYamlService, ) { this.ruleConstants = new RuleConstants(); } @@ -119,8 +121,8 @@ export class RulesService { libraryId !== undefined ? `rg.libraryId = ${libraryId}` : typeId !== undefined - ? `c.type = ${typeId}` - : 'rg.libraryId != -1', + ? `c.type = ${typeId}` + : 'rg.libraryId != -1', ) // .where(typeId !== undefined ? `c.type = ${typeId}` : '') .getMany(); @@ -189,8 +191,8 @@ export class RulesService { lib.type === 'movie' ? EPlexDataType.MOVIES : params.dataType !== undefined - ? params.dataType - : EPlexDataType.SHOWS, + ? params.dataType + : EPlexDataType.SHOWS, title: params.name, description: params.description, arrAction: params.arrAction ? params.arrAction : 0, @@ -699,4 +701,12 @@ export class RulesService { ); } } + + public encodeToYaml(rules: RuleDto[], mediaType: number): ReturnStatus { + return this.ruleYamlService.encode(rules, mediaType); + } + + public decodeFromYaml(yaml: string, mediaType: number): ReturnStatus { + return this.ruleYamlService.decode(yaml, mediaType); + } } diff --git a/ui/src/components/Common/YamlImporterModal/index.tsx b/ui/src/components/Common/YamlImporterModal/index.tsx new file mode 100644 index 00000000..e05a88f6 --- /dev/null +++ b/ui/src/components/Common/YamlImporterModal/index.tsx @@ -0,0 +1,61 @@ +import { useRef } from 'react' +import Modal from '../Modal' +import Editor from '@monaco-editor/react' + +export interface IYamlImporterModal { + onImport: (yaml: string) => void + onCancel: () => void + yaml?: string +} + +const YamlImporterModal = (props: IYamlImporterModal) => { + const editorRef = useRef(undefined) + + function handleEditorDidMount(editor: any, monaco: any) { + editorRef.current = editor + } + + const download = async () => { + if (props.yaml) { + const blob = new Blob([props.yaml], { + type: 'text/yaml', + }) + const href = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = href + link.download = `maintainerr_rules_${new Date().getTime()}.yaml` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + } + + return ( +
+ props.onCancel()} + okDisabled={false} + onOk={ + props.yaml + ? () => download() + : () => props.onImport((editorRef.current as any).getValue()) + } + okText={props.yaml ? 'Download' : 'Import'} + okButtonType={'primary'} + title={'Rule Yaml editor'} + iconSvg={''} + > + + +
+ ) +} +export default YamlImporterModal diff --git a/ui/src/components/Rules/RuleGroup/AddModal/index.tsx b/ui/src/components/Rules/RuleGroup/AddModal/index.tsx index 5b42b54e..68355d1b 100644 --- a/ui/src/components/Rules/RuleGroup/AddModal/index.tsx +++ b/ui/src/components/Rules/RuleGroup/AddModal/index.tsx @@ -17,6 +17,7 @@ import { DownloadIcon, QuestionMarkCircleIcon, SaveIcon, + UploadIcon, } from '@heroicons/react/solid' import Router from 'next/router' import Link from 'next/link' @@ -24,6 +25,9 @@ import Button from '../../../Common/Button' import CommunityRuleModal from '../../../Common/CommunityRuleModal' import { EPlexDataType } from '../../../../utils/PlexDataType-enum' import CachedImage from '../../../Common/CachedImage' +import YamlImporterModal from '../../../Common/YamlImporterModal' +import { CloudDownloadIcon } from '@heroicons/react/outline' +import { useToasts } from 'react-toast-notifications' interface AddModal { editData?: IRuleGroup @@ -52,15 +56,19 @@ interface ICreateApiObject { const AddModal = (props: AddModal) => { const [selectedLibraryId, setSelectedLibraryId] = useState( - props.editData ? props.editData.libraryId.toString() : '1' + props.editData ? props.editData.libraryId.toString() : '1', ) const [selectedType, setSelectedType] = useState( - props.editData && props.editData.type ? props.editData.type.toString() : '1' + props.editData && props.editData.type + ? props.editData.type.toString() + : '1', ) const [selectedLibrary, setSelectedLibrary] = useState() const [collection, setCollection] = useState() const [isLoading, setIsLoading] = useState(true) const [CommunityModal, setCommunityModal] = useState(false) + const [yamlImporterModal, setYamlImporterModal] = useState(false) + const yaml = useRef() const nameRef = useRef() const descriptionRef = useRef() @@ -74,20 +82,22 @@ const AddModal = (props: AddModal) => { const [manualCollection, setManualCollection] = useState(false) const [manualCollectionName, setManualCollectionName] = useState( - 'My custom collection' + 'My custom collection', ) + const { addToast } = useToasts() + const [useRules, setUseRules] = useState( - props.editData ? props.editData.useRules : true + props.editData ? props.editData.useRules : true, ) const [arrOption, setArrOption] = useState() const [active, setActive] = useState( - props.editData ? props.editData.isActive : true + props.editData ? props.editData.isActive : true, ) const [rules, setRules] = useState( props.editData ? props.editData.rules.map((r) => JSON.parse(r.ruleJson) as IRule) - : [] + : [], ) const [error, setError] = useState(false) const [formIncomplete, setFormIncomplete] = useState(false) @@ -118,27 +128,70 @@ const AddModal = (props: AddModal) => { } } + const toggleYamlExporter = async (e: any) => { + e.preventDefault() + const response = await PostApiHandler('/rules/yaml/encode', { + rules: JSON.stringify(rules), + mediaType: selectedType, + }) + + if (response.code === 1) { + yaml.current = response.result + + if (!yamlImporterModal) { + setYamlImporterModal(true) + } else { + setYamlImporterModal(false) + } + } + } + + const toggleYamlImporter = (e: any) => { + e.preventDefault() + yaml.current = undefined + if (!yamlImporterModal) { + setYamlImporterModal(true) + } else { + setYamlImporterModal(false) + } + } + + const importRulesFromYaml = async (yaml: string) => { + const response = await PostApiHandler('/rules/yaml/decode', { + yaml: yaml, + mediaType: selectedType, + }) + + if (response && response.code === 1) { + const result: { mediaType: string; rules: IRule[] } = JSON.parse( + response.result, + ) + handleLoadRules(result.rules) + addToast('Successfully imported rules from Yaml', { + autoDismiss: true, + appearance: 'success', + }) + } else { + addToast(response.message, { + autoDismiss: true, + appearance: 'error', + }) + } + } + const handleLoadRules = (rules: IRule[]) => { updateRules(rules) ruleCreatorVersion.current = ruleCreatorVersion.current + 1 setCommunityModal(false) } - const loadJsonRules = (rules: string[]) => { - const transformedRules: IRule[] = rules - ? rules.map((r) => JSON.parse(r) as IRule) - : [] - updateRules(transformedRules) - ruleCreatorVersion.current = ruleCreatorVersion.current + 1 - } - const cancel = () => { props.onCancel() } useEffect(() => { const lib = LibrariesCtx.libraries.find( - (el: ILibrary) => +el.key === +selectedLibraryId + (el: ILibrary) => +el.key === +selectedLibraryId, ) setSelectedLibrary(lib) setSelectedType(lib?.type === 'movie' ? '1' : '2') @@ -157,7 +210,7 @@ const AddModal = (props: AddModal) => { } if (props.editData) { GetApiHandler( - `/collections/collection/${props.editData.collectionId}` + `/collections/collection/${props.editData.collectionId}`, ).then((resp: ICollection) => { resp ? setCollection(resp) : undefined resp ? setShowHome(resp.visibleOnHome!) : undefined @@ -421,31 +474,31 @@ const AddModal = (props: AddModal) => { }, ] : +selectedType === EPlexDataType.SEASONS - ? [ - { - id: 0, - name: 'Unmonitor and delete season', - }, - { - id: 2, - name: 'Unmonitor and Delete existing episodes', - }, - { - id: 3, - name: 'Unmonitor season and keep files', - }, - ] - : // episodes - [ - { - id: 0, - name: 'Unmonitor and delete episode', - }, - { - id: 3, - name: 'Unmonitor and keep file', - }, - ] + ? [ + { + id: 0, + name: 'Unmonitor and delete season', + }, + { + id: 2, + name: 'Unmonitor and Delete existing episodes', + }, + { + id: 3, + name: 'Unmonitor season and keep files', + }, + ] + : // episodes + [ + { + id: 0, + name: 'Unmonitor and delete episode', + }, + { + id: 3, + name: 'Unmonitor and keep file', + }, + ] } /> @@ -453,7 +506,11 @@ const AddModal = (props: AddModal) => {
@@ -636,11 +693,11 @@ const AddModal = (props: AddModal) => {
+
+ + + +
{CommunityModal ? ( { onCancel={() => setCommunityModal(false)} /> ) : undefined} + {yamlImporterModal ? ( + { + importRulesFromYaml(yaml) + setYamlImporterModal(false) + }} + onCancel={() => { + setYamlImporterModal(false) + }} + /> + ) : undefined} { /> -
{/* */}
diff --git a/ui/src/pages/_app.tsx b/ui/src/pages/_app.tsx index fca5d1a4..6833462f 100644 --- a/ui/src/pages/_app.tsx +++ b/ui/src/pages/_app.tsx @@ -5,6 +5,7 @@ import axios from 'axios' import { LibrariesContextProvider } from '../contexts/libraries-context' import { SettingsContextProvider } from '../contexts/settings-context' import { SearchContextProvider } from '../contexts/search-context' +import { ToastProvider } from 'react-toast-notifications' axios.defaults.headers.common['Access-Control-Allow-Origin'] = '*' function CoreApp({ Component, pageProps }: AppProps) { @@ -13,7 +14,9 @@ function CoreApp({ Component, pageProps }: AppProps) { - + + + diff --git a/yarn.lock b/yarn.lock index da6510f5..d615f48e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -256,7 +256,7 @@ dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== @@ -1109,6 +1109,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.7.2": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1160,6 +1167,83 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + +"@emotion/core@^10.0.14": + version "10.3.1" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.3.1.tgz#4021b6d8b33b3304d48b0bb478485e7d7421c69d" + integrity sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1213,6 +1297,13 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@headlessui/react@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.12.tgz#9ab2baa3c4f632782631e00937f9531a34033619" + integrity sha512-FhSx5V+Qp0GvbTpaxyS+ymGDDNntCacClWsk/d8Upbr19g3AsPbjfPk4+m2CgJGcuCB5Dz7LpUIOAbvQTyjL2g== + dependencies: + client-only "^0.0.1" + "@heroicons/react@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324" @@ -1537,6 +1628,20 @@ semver "^7.3.5" tar "^6.1.11" +"@monaco-editor/loader@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558" + integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg== + dependencies: + state-local "^1.0.6" + +"@monaco-editor/react@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119" + integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw== + dependencies: + "@monaco-editor/loader" "^1.4.0" + "@nestjs/cli@^10.2.1": version "10.2.1" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.2.1.tgz#a1d32c28e188f0fb4c3f54235c55745de4c6dd7f" @@ -2608,6 +2713,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + "@types/prop-types@*": version "15.7.9" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" @@ -3485,6 +3595,22 @@ babel-jest@^29.7.0: graceful-fs "^4.2.9" slash "^3.0.0" +babel-plugin-emotion@^10.0.27: + version "10.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d" + integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -3506,6 +3632,15 @@ babel-plugin-jest-hoist@^29.6.3: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" +babel-plugin-macros@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + babel-plugin-polyfill-corejs2@^0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" @@ -3555,6 +3690,11 @@ babel-plugin-react-intl@^8.2.25: schema-utils "^3.0.0" tslib "^2.0.1" +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -4311,6 +4451,11 @@ conventional-commits-parser@^5.0.0: meow "^12.0.1" split2 "^4.0.0" +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -4361,6 +4506,17 @@ corser@^2.0.1: resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ== +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + cosmiconfig@^8.0.0, cosmiconfig@^8.1.3, cosmiconfig@^8.2.0: version "8.3.6" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" @@ -4467,6 +4623,11 @@ csso@^5.0.5: dependencies: css-tree "~2.2.0" +csstype@^2.5.7: + version "2.6.21" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" + integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== + csstype@^3.0.2: version "3.1.2" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" @@ -5479,6 +5640,11 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -6275,7 +6441,7 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.2.1, import-fresh@^3.3.0: +import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -9412,7 +9578,15 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-transition-group@^4.4.5: +react-toast-notifications@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/react-toast-notifications/-/react-toast-notifications-2.5.1.tgz#30216eedb5608ec69719a818b9a2e09283e90074" + integrity sha512-eYuuiSPGLyuMHojRH2U7CbENvFHsvNia39pLM/s10KipIoNs14T7RIJk4aU2N+l++OsSgtJqnFObx9bpwLMU5A== + dependencies: + "@emotion/core" "^10.0.14" + react-transition-group "^4.4.1" + +react-transition-group@^4.4.1, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== @@ -9717,7 +9891,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.4: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -10173,6 +10347,11 @@ source-map@0.7.4, source-map@^0.7.4: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -10273,6 +10452,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +state-local@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5" + integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -11510,11 +11694,21 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yaml@^2.1.1: version "2.3.3" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== +yaml@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"