From 21d07c91e932498f89e0e5e95dd9e4a075b5de74 Mon Sep 17 00:00:00 2001 From: Leire Date: Wed, 11 Dec 2024 12:20:25 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8E=89=20Improve=20styles=20file=20we?= =?UTF-8?q?ight=20(#5724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the weight of the style files by removing some css files from the scss variable bundle. FINAL: ![Screenshot 2024-12-11 at 11 55 33](https://github.com/user-attachments/assets/097dbd99-6d18-4109-b5ed-4a02fdabc8c5) --------- Co-authored-by: Damián Pumar --- argilla-frontend/CHANGELOG.md | 1 + .../fonts/fonts.scss => css/fonts.css} | 0 .../variables/_themes.scss => css/themes.css} | 0 argilla-frontend/assets/scss/abstract.scss | 2 - .../scss/abstract/mixins/_media-queries.scss | 468 +++++++++--------- argilla-frontend/assets/scss/base/base.scss | 1 + .../base/base-banner/BaseBanner.vue | 4 +- .../base/base-progress/BaseLinearProgress.vue | 4 +- .../base/base-search-bar/BaseSearchBar.vue | 6 +- .../container/fields/sandbox/Sandbox.vue | 5 +- .../components/EntityComponent.vue | 1 - .../components/EntityDropdown.vue | 1 - .../components/EntityDropdownOverlapping.vue | 1 - .../annotation/settings/SettingsInfo.vue | 4 +- .../settings/SettingsInfoReadOnly.vue | 4 +- argilla-frontend/nuxt.config.ts | 61 ++- argilla-frontend/package.json | 1 + .../pages/dataset/_id/settings.vue | 4 +- .../services/useLanguageDetector.ts | 1 + 19 files changed, 314 insertions(+), 255 deletions(-) rename argilla-frontend/assets/{scss/abstract/fonts/fonts.scss => css/fonts.css} (100%) rename argilla-frontend/assets/{scss/abstract/variables/_themes.scss => css/themes.css} (100%) diff --git a/argilla-frontend/CHANGELOG.md b/argilla-frontend/CHANGELOG.md index 93e4256b82..3bc635ca4e 100644 --- a/argilla-frontend/CHANGELOG.md +++ b/argilla-frontend/CHANGELOG.md @@ -18,6 +18,7 @@ These are the section headers that we use: ### Fixed +- Improved performance and accessibility ([#5724](https://github.com/argilla-io/argilla/pull/5724)) - Fixed dataset update date information in the dataset list ([#5741](https://github.com/argilla-io/argilla/pull/#5741)) ## [2.5.0](https://github.com/argilla-io/argilla/compare/v2.4.1...v2.5.0) diff --git a/argilla-frontend/assets/scss/abstract/fonts/fonts.scss b/argilla-frontend/assets/css/fonts.css similarity index 100% rename from argilla-frontend/assets/scss/abstract/fonts/fonts.scss rename to argilla-frontend/assets/css/fonts.css diff --git a/argilla-frontend/assets/scss/abstract/variables/_themes.scss b/argilla-frontend/assets/css/themes.css similarity index 100% rename from argilla-frontend/assets/scss/abstract/variables/_themes.scss rename to argilla-frontend/assets/css/themes.css diff --git a/argilla-frontend/assets/scss/abstract.scss b/argilla-frontend/assets/scss/abstract.scss index 58b4f002f0..c49350b567 100644 --- a/argilla-frontend/assets/scss/abstract.scss +++ b/argilla-frontend/assets/scss/abstract.scss @@ -16,10 +16,8 @@ */ // abstract -@import "abstract/fonts/fonts"; @import "abstract/functions/functions"; @import "abstract/variables/variables"; -@import "abstract/variables/themes"; @import "abstract/mixins/mixins"; @import "abstract/mixins/media-queries"; @import "abstract/mixins/grid-mixins"; diff --git a/argilla-frontend/assets/scss/abstract/mixins/_media-queries.scss b/argilla-frontend/assets/scss/abstract/mixins/_media-queries.scss index 131e764b90..8d50cb951b 100644 --- a/argilla-frontend/assets/scss/abstract/mixins/_media-queries.scss +++ b/argilla-frontend/assets/scss/abstract/mixins/_media-queries.scss @@ -16,33 +16,20 @@ */ @charset "UTF-8"; -// _ _ _ _ _ -// (_) | | | | | (_) -// _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _ -// | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` | -// | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| | -// |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_| -// -// Simple, elegant and maintainable media queries in Sass -// v1.4.9 -// -// http://include-media.com -// -// Authors: Eduardo Boucas (@eduardoboucas) -// Hugo Giraudel (@hugogiraudel) -// -// This project is licensed under the terms of the MIT license -//// -/// include-media library public configuration -/// @author Eduardo Boucas -/// @access public -//// -/// -/// Creates a list of global breakpoints -/// -/// @example scss - Creates a single breakpoint with the label `phone` -/// $breakpoints: ('phone': 320px); -/// +// Simple, elegant and maintainable media queries in Sass +// v1.4.9 +// http://include-media.com +// Authors: Eduardo Boucas (@eduardoboucas) +// Hugo Giraudel (@hugogiraudel) +// This project is licensed under the terms of the MIT license + +// include-media library public configuration +// @author Eduardo Boucas +// @access public + +// Creates a list of global breakpoints +// @example scss - Creates a single breakpoint with the label `phone` +// $breakpoints: ('phone': 320px); $breakpoints: ( "phone": 320px, "tablet": 768px, @@ -50,17 +37,14 @@ $breakpoints: ( "desktopLarge": 1440px, "xxl": 1900px, ) !default; -/// -/// Creates a list of static expressions or media types -/// -/// @example scss - Creates a single media type (screen) -/// $media-expressions: ('screen': 'screen'); -/// -/// @example scss - Creates a static expression with logical disjunction (OR operator) -/// $media-expressions: ( -/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' -/// ); -/// + +// Creates a list of static expressions or media types +// @example scss - Creates a single media type (screen) +// $media-expressions: ('screen': 'screen'); +// @example scss - Creates a static expression with logical disjunction (OR operator) +// $media-expressions: ( +// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' +// ); $media-expressions: ( "screen": "screen", "print": "print", @@ -72,125 +56,102 @@ $media-expressions: ( "retina3x": "(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)", ) !default; -/// -/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals -/// -/// @example scss - Interval for pixels is defined as `1` by default -/// @include media('>128px') {} -/// -/// /* Generates: */ -/// @media (min-width: 129px) {} -/// -/// @example scss - Interval for ems is defined as `0.01` by default -/// @include media('>20em') {} -/// -/// /* Generates: */ -/// @media (min-width: 20.01em) {} -/// -/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` -/// @include media('>2.0rem') {} -/// -/// /* Generates: */ -/// @media (min-width: 2.1rem) {} -/// + +// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals +// @example scss - Interval for pixels is defined as `1` by default +// @include media('>128px') {} +// /* Generates: */ +// @media (min-width: 129px) {} +// @example scss - Interval for ems is defined as `0.01` by default +// @include media('>20em') {} +// /* Generates: */ +// @media (min-width: 20.01em) {} +// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` +// @include media('>2.0rem') {} +// /* Generates: */ +// @media (min-width: 2.1rem) {} $unit-intervals: ( "px": 1, "em": 0.01, "rem": 0.1, "": 0, ) !default; -/// -/// Defines whether support for media queries is available, useful for creating separate stylesheets -/// for browsers that don't support media queries. -/// -/// @example scss - Disables support for media queries -/// $im-media-support: false; -/// @include media('>=tablet') { -/// .foo { -/// color: tomato; -/// } -/// } -/// -/// /* Generates: */ -/// .foo { -/// color: tomato; -/// } -/// + +// Defines whether support for media queries is available, useful for creating separate stylesheets +// for browsers that don't support media queries. +// @example scss - Disables support for media queries +// $im-media-support: false; +// @include media('>=tablet') { +// .foo { +// color: tomato; +// } +// } +// /* Generates: */ +// .foo { +// color: tomato; +// } $im-media-support: true !default; -/// -/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or -/// intercept the breakpoint will be displayed, any others will be ignored. -/// -/// @example scss - This media query will show because it intercepts the static breakpoint -/// $im-media-support: false; -/// $im-no-media-breakpoint: 'desktop'; -/// @include media('>=tablet') { -/// .foo { -/// color: tomato; -/// } -/// } -/// -/// /* Generates: */ -/// .foo { -/// color: tomato; -/// } -/// -/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint -/// $im-media-support: false; -/// $im-no-media-breakpoint: 'tablet'; -/// @include media('>=desktop') { -/// .foo { -/// color: tomato; -/// } -/// } -/// -/// /* No output */ -/// + +// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or +// intercept the breakpoint will be displayed, any others will be ignored. +// @example scss - This media query will show because it intercepts the static breakpoint +// $im-media-support: false; +// $im-no-media-breakpoint: 'desktop'; +// @include media('>=tablet') { +// .foo { +// color: tomato; +// } +// } +// /* Generates: */ +// .foo { +// color: tomato; +// } +// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint +// $im-media-support: false; +// $im-no-media-breakpoint: 'tablet'; +// @include media('>=desktop') { +// .foo { +// color: tomato; +// } +// } +// /* No output */ $im-no-media-breakpoint: "desktop" !default; -/// -/// Selects which media expressions are allowed in an expression for it to be used when media queries -/// are not supported. -/// -/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions -/// $im-media-support: false; -/// $im-no-media-breakpoint: 'desktop'; -/// $im-no-media-expressions: ('screen'); -/// @include media('>=tablet', 'screen') { -/// .foo { -/// color: tomato; -/// } -/// } -/// -/// /* Generates: */ -/// .foo { -/// color: tomato; -/// } -/// -/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted -/// $im-media-support: false; -/// $im-no-media-breakpoint: 'desktop'; -/// $im-no-media-expressions: ('screen'); -/// @include media('>=tablet', 'retina2x') { -/// .foo { -/// color: tomato; -/// } -/// } -/// -/// /* No output */ -/// + +// Selects which media expressions are allowed in an expression for it to be used when media queries +// are not supported. +// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions +// $im-media-support: false; +// $im-no-media-breakpoint: 'desktop'; +// $im-no-media-expressions: ('screen'); +// @include media('>=tablet', 'screen') { +// .foo { +// color: tomato; +// } +// } +// /* Generates: */ +// .foo { +// color: tomato; +// } +// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted +// $im-media-support: false; +// $im-no-media-breakpoint: 'desktop'; +// $im-no-media-expressions: ('screen'); +// @include media('>=tablet', 'retina2x') { +// .foo { +// color: tomato; +// } +// } +// /* No output */ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; -//// -/// Cross-engine logging engine -/// @author Hugo Giraudel -/// @access private -//// -/// -/// Log a message either with `@error` if supported -/// else with `@warn`, using `feature-exists('at-error')` -/// to detect support. -/// -/// @param {String} $message - Message to log -/// + +// Cross-engine logging engine +// @author Hugo Giraudel +// @access private + +// Log a message either with `@error` if supported +// else with `@warn`, using `feature-exists('at-error')` +// to detect support. +// @param {String} $message - Message to log @function im-log($message) { @if feature-exists("at-error") { @error $message; @@ -201,33 +162,24 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return $message; } -/// -/// Wrapper mixin for the log function so it can be used with a more friendly -/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function -/// within functions because it is not possible to include a mixin in a function -/// and use the mixin everywhere else because it's much more elegant. -/// -/// @param {String} $message - Message to log -/// +// Wrapper mixin for the log function so it can be used with a more friendly +// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function +// within functions because it is not possible to include a mixin in a function +// and use the mixin everywhere else because it's much more elegant. +// @param {String} $message - Message to log @mixin log($message) { @if im-log($message) { } } -/// -/// Function with no `@return` called next to `@warn` in Sass 3.3 -/// to trigger a compiling error and stop the process. -/// +// Function with no `@return` called next to `@warn` in Sass 3.3 +// to trigger a compiling error and stop the process. @function noop() { } -/// -/// Determines whether a list of conditions is intercepted by the static breakpoint. -/// -/// @param {Arglist} $conditions - Media query conditions -/// -/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint -/// +// Determines whether a list of conditions is intercepted by the static breakpoint. +// @param {Arglist} $conditions - Media query conditions +// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint @function im-intercepts-static-breakpoint($conditions...) { $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); @if not $no-media-breakpoint-value { @@ -251,24 +203,20 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return true; } -//// -/// Parsing engine -/// @author Hugo Giraudel -/// @access private -//// -/// -/// Get operator of an expression -/// -/// @param {String} $expression - Expression to extract operator from -/// -/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` -/// +// Parsing engine +// @author Hugo Giraudel +// @access private + +// Get operator of an expression +// @param {String} $expression - Expression to extract operator from +// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` @function get-expression-operator($expression) { @each $operator in (">=", ">", "<=", "<", "≥", "≤") { @if str-index($expression, $operator) { @return $operator; } - } // It is not possible to include a mixin inside a function, so we have to + } + // It is not possible to include a mixin inside a function, so we have to // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because // functions cannot be called anywhere in Sass, we need to hack the call in // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with @@ -276,14 +224,10 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; $_: im-log("No operator found in `#{$expression}`."); } -/// -/// Get dimension of an expression, based on a found operator -/// -/// @param {String} $expression - Expression to extract dimension from -/// @param {String} $operator - Operator from `$expression` -/// -/// @return {String} - `width` or `height` (or potentially anything else) -/// +// Get dimension of an expression, based on a found operator +// @param {String} $expression - Expression to extract dimension from +// @param {String} $operator - Operator from `$expression` +// @return {String} - `width` or `height` (or potentially anything else) @function get-expression-dimension($expression, $operator) { $operator-index: str-index($expression, $operator); $parsed-dimension: str-slice($expression, 0, $operator-index - 1); @@ -294,25 +238,17 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return $dimension; } -/// -/// Get dimension prefix based on an operator -/// -/// @param {String} $operator - Operator -/// -/// @return {String} - `min` or `max` -/// +// Get dimension prefix based on an operator +// @param {String} $operator - Operator +// @return {String} - `min` or `max` @function get-expression-prefix($operator) { @return if(index(("<", "<=", "≤"), $operator), "max", "min"); } -/// -/// Get value of an expression, based on a found operator -/// -/// @param {String} $expression - Expression to extract value from -/// @param {String} $operator - Operator from `$expression` -/// -/// @return {Number} - A numeric value -/// +// Get value of an expression, based on a found operator +// @param {String} $expression - Expression to extract value from +// @param {String} $operator - Operator from `$expression` +// @return {Number} - A numeric value @function get-expression-value($expression, $operator) { $operator-index: str-index($expression, $operator); $value: str-slice($expression, $operator-index + str-length($operator)); @@ -338,13 +274,9 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return $value; } -/// -/// Parse an expression to return a valid media-query expression -/// -/// @param {String} $expression - Expression to parse -/// -/// @return {String} - Valid media query -/// +// Parse an expression to return a valid media-query expression +// @param {String} $expression - Expression to parse +// @return {String} - Valid media query @function parse-expression($expression) { // If it is part of $media-expressions, it has no operator // then there is no need to go any further, just return the value @@ -358,17 +290,12 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return "(#{$prefix}-#{$dimension}: #{$value})"; } -/// -/// Slice `$list` between `$start` and `$end` indexes -/// -/// @access private -/// -/// @param {List} $list - List to slice -/// @param {Number} $start [1] - Start index -/// @param {Number} $end [length($list)] - End index -/// -/// @return {List} Sliced list -/// +// Slice `$list` between `$start` and `$end` indexes +// @access private +// @param {List} $list - List to slice +// @param {Number} $start [1] - Start index +// @param {Number} $end [length($list)] - End index +// @return {List} Sliced list @function slice($list, $start: 1, $end: length($list)) { @if length($list) < 1 or $start>$end { @return (); @@ -380,18 +307,13 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return $result; } -//// -/// String to number converter -/// @author Hugo Giraudel -/// @access private -//// -/// -/// Casts a string into a number -/// -/// @param {String | Number} $value - Value to be parsed -/// -/// @return {Number} -/// +// String to number converter +// @author Hugo Giraudel +// @access private + +// Casts a string into a number +// @param {String | Number} $value - Value to be parsed +// @return {Number} @function to-number($value) { @if type-of($value) == "number" { @return $value; @@ -413,7 +335,8 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; "7": 7, "8": 8, "9": 9, - ); // Remove +/- sign if present at first character + ); + // Remove +/- sign if present at first character @if ($first-character== "+" or $first-character== "-") { $value: str-slice($value, 2); } @@ -434,6 +357,83 @@ $im-no-media-expressions: ("screen", "portrait", "landscape") !default; @return if($minus, -$result, $result); } +// Add `$unit` to `$value` +// @param {Number} $value - Value to add unit to +// @param {String} $unit - String representation of the unit +// @return {Number} - `$value` expressed in `$unit` +@function to-length($value, $unit) { + $units: ( + "px": 1px, + "cm": 1cm, + "mm": 1mm, + "%": 1%, + "ch": 1ch, + "pc": 1pc, + "in": 1in, + "em": 1em, + "rem": 1rem, + "pt": 1pt, + "ex": 1ex, + "vw": 1vw, + "vh": 1vh, + "vmin": 1vmin, + "vmax": 1vmax, + ); + @if not index(map-keys($units), $unit) { + $_: im-log("Invalid unit `#{$unit}`."); + } + @return $value * map-get($units, $unit); +} + +// This mixin aims at redefining the configuration just for the scope of +// the call. It is helpful when having a component needing an extended +// configuration such as custom breakpoints (referred to as tweakpoints) +// for instance. +// @author Hugo Giraudel +// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` +// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` +// @example scss - Extend the global breakpoints with a tweakpoint +// @include media-context(('custom': 678px)) { +// .foo { +// @include media('>phone', '<=custom') { +// // ... +// } +// } +// } +// @example scss - Extend the global media expressions with a custom one +// @include media-context($tweak-media-expressions: ('all': 'all')) { +// .foo { +// @include media('all', '>phone') { +// // ... +// } +// } +// } +// @example scss - Extend both configuration maps +// @include media-context(('custom': 678px), ('all': 'all')) { +// .foo { +// @include media('all', '>phone', '<=custom') { +// // ... +// } +// } +// } +@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { + // Save global configuration + $global-breakpoints: $breakpoints; + $global-media-expressions: $media-expressions; + // Update global configuration + $breakpoints: map-merge($breakpoints, $tweakpoints) !global; + $media-expressions: map-merge( + $media-expressions, + $tweak-media-expressions + ) !global; + @content; + // Restore global configuration + $breakpoints: $global-breakpoints !global; + $media-expressions: $global-media-expressions !global; +} + +// include-media public exposed API +// @author Eduardo Boucas /// /// Add `$unit` to `$value` /// diff --git a/argilla-frontend/assets/scss/base/base.scss b/argilla-frontend/assets/scss/base/base.scss index 515b5e505f..6a5f1f1f96 100644 --- a/argilla-frontend/assets/scss/base/base.scss +++ b/argilla-frontend/assets/scss/base/base.scss @@ -16,6 +16,7 @@ */ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Roboto+Condensed&display=swap"); html { box-sizing: border-box; -webkit-text-size-adjust: 100%; diff --git a/argilla-frontend/components/base/base-banner/BaseBanner.vue b/argilla-frontend/components/base/base-banner/BaseBanner.vue index 0169d79832..6a091905c8 100644 --- a/argilla-frontend/components/base/base-banner/BaseBanner.vue +++ b/argilla-frontend/components/base/base-banner/BaseBanner.vue @@ -77,7 +77,7 @@ export default { }; - + diff --git a/argilla-frontend/components/base/base-progress/BaseLinearProgress.vue b/argilla-frontend/components/base/base-progress/BaseLinearProgress.vue index cda4e92796..befe7e07d1 100644 --- a/argilla-frontend/components/base/base-progress/BaseLinearProgress.vue +++ b/argilla-frontend/components/base/base-progress/BaseLinearProgress.vue @@ -102,7 +102,7 @@ export default { }; - + diff --git a/argilla-frontend/components/base/base-search-bar/BaseSearchBar.vue b/argilla-frontend/components/base/base-search-bar/BaseSearchBar.vue index f379bd5f51..4a91845321 100644 --- a/argilla-frontend/components/base/base-search-bar/BaseSearchBar.vue +++ b/argilla-frontend/components/base/base-search-bar/BaseSearchBar.vue @@ -19,7 +19,11 @@ + > diff --git a/argilla-frontend/components/features/annotation/settings/SettingsInfoReadOnly.vue b/argilla-frontend/components/features/annotation/settings/SettingsInfoReadOnly.vue index 853890c864..435fbc1009 100644 --- a/argilla-frontend/components/features/annotation/settings/SettingsInfoReadOnly.vue +++ b/argilla-frontend/components/features/annotation/settings/SettingsInfoReadOnly.vue @@ -69,7 +69,7 @@ export default { }; - + diff --git a/argilla-frontend/nuxt.config.ts b/argilla-frontend/nuxt.config.ts index ece2d5dd31..7317400a55 100644 --- a/argilla-frontend/nuxt.config.ts +++ b/argilla-frontend/nuxt.config.ts @@ -53,7 +53,11 @@ const config: NuxtConfig = { }, // Global CSS (https://go.nuxtjs.dev/config-css) - css: ["~assets/scss/base/base.scss"], + css: [ + "~assets/css/fonts.css", + "~assets/css/themes.css", + "~assets/scss/base/base.scss", + ], // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins) plugins: [{ src: "~/plugins" }], @@ -73,7 +77,23 @@ const config: NuxtConfig = { // https://go.nuxtjs.dev/typescript "@nuxt/typescript-build", "@nuxtjs/composition-api/module", - ["@pinia/nuxt", { disableVuex: false }], + [ + "@pinia/nuxt", + { + disableVuex: false, + }, + ], + [ + "nuxt-compress", + { + gzip: { + cache: true, + }, + brotli: { + threshold: 10240, + }, + }, + ], ], // Modules (https://go.nuxtjs.dev/config-modules) @@ -129,10 +149,8 @@ const config: NuxtConfig = { target: BASE_URL, }, }, - // Build Configuration (https://go.nuxtjs.dev/config-build) build: { - cssSourceMap: false, extend(config) { config.resolve.alias.vue = "vue/dist/vue.common"; config.module.rules.push({ @@ -143,6 +161,26 @@ const config: NuxtConfig = { }, }); }, + postcss: { + postcssOptions: { + order: "presetEnvAndCssnanoLast", + plugins: { + cssnano: + process.env.NODE_ENV === "production" + ? { + preset: [ + "default", + { + discardComments: { + removeAll: true, + }, + }, + ], + } + : false, + }, + }, + }, babel: { plugins: [["@babel/plugin-proposal-private-methods", { loose: true }]], }, @@ -152,6 +190,21 @@ const config: NuxtConfig = { keep_fnames: true, }, }, + extractCSS: true, + splitChunks: { + pages: true, + commons: true, + layouts: true, + }, + optimization: { + splitChunks: { + name: false, + }, + }, + filenames: { + css: ({ isDev }) => (isDev ? "[name].css" : "[contenthash].css"), + }, + publicPath: "/_nuxt/", }, // https://github.com/nuxt-community/style-resources-module diff --git a/argilla-frontend/package.json b/argilla-frontend/package.json index d3171965e1..0a1325ddfc 100644 --- a/argilla-frontend/package.json +++ b/argilla-frontend/package.json @@ -74,6 +74,7 @@ "jest": "^27.4.5", "jest-serializer-vue": "^2.0.2", "jest-transform-stub": "^2.0.0", + "nuxt-compress": "5.0.0", "prettier": "^2.2.1", "sass-loader": "^10.1.0", "typescript": "^5.1.6", diff --git a/argilla-frontend/pages/dataset/_id/settings.vue b/argilla-frontend/pages/dataset/_id/settings.vue index b2b9068386..29631c33cb 100644 --- a/argilla-frontend/pages/dataset/_id/settings.vue +++ b/argilla-frontend/pages/dataset/_id/settings.vue @@ -53,7 +53,7 @@ export default { }; - + diff --git a/argilla-frontend/v1/infrastructure/services/useLanguageDetector.ts b/argilla-frontend/v1/infrastructure/services/useLanguageDetector.ts index 8482ed6da9..7ebd6aef0c 100644 --- a/argilla-frontend/v1/infrastructure/services/useLanguageDetector.ts +++ b/argilla-frontend/v1/infrastructure/services/useLanguageDetector.ts @@ -52,6 +52,7 @@ export const useLanguageChanger = (context: Context) => { const change = (language: string) => { i18n.setLocale(language); + document.documentElement.lang = language; set("language", language); }; From 04879366606c330a8c3909b2046f0407d7dbcf98 Mon Sep 17 00:00:00 2001 From: Paco Aranda Date: Wed, 11 Dec 2024 14:45:23 +0100 Subject: [PATCH 2/3] [FEATURE] Add support to update record fields (#5685) # Description This PR adds backend support to update record fields. **Type of change** - Improvement (change adding some improvement to an existing functionality) **How Has This Been Tested** **Checklist** - I added relevant documentation - I followed the style guidelines of this project - I did a self-review of my code - I made corresponding changes to the documentation - I confirm My changes generate no new warnings - I have added tests that prove my fix is effective or that my feature works - I have added relevant notes to the CHANGELOG.md file (See https://keepachangelog.com/) --- argilla-server/CHANGELOG.md | 5 + .../api/handlers/v1/datasets/records.py | 2 +- .../argilla_server/api/handlers/v1/records.py | 15 +- .../argilla_server/api/schemas/v1/records.py | 29 +- .../api/schemas/v1/records_bulk.py | 2 +- .../src/argilla_server/bulk/records_bulk.py | 24 +- .../src/argilla_server/contexts/datasets.py | 345 +----------------- .../src/argilla_server/contexts/records.py | 127 ++++++- .../src/argilla_server/validators/records.py | 28 +- .../test_upsert_dataset_records_bulk.py | 83 ++++- .../handlers/v1/records/test_update_record.py | 2 +- .../unit/api/handlers/v1/test_records.py | 107 +++++- argilla-server/tests/unit/conftest.py | 1 - .../tests/integration/test_update_records.py | 30 ++ 14 files changed, 397 insertions(+), 403 deletions(-) diff --git a/argilla-server/CHANGELOG.md b/argilla-server/CHANGELOG.md index c99887b1c4..91c4f39367 100644 --- a/argilla-server/CHANGELOG.md +++ b/argilla-server/CHANGELOG.md @@ -16,6 +16,11 @@ These are the section headers that we use: ## [Unreleased]() +### Added + +- Added support to update record fields in `PATCH /api/v1/records/:record_id` endpoint. ([#5685](https://github.com/argilla-io/argilla/pull/5685)) +- Added support to update record fields in `PUT /api/v1/datasets/:dataset_id/records/bulk` endpoint. ([#5685](https://github.com/argilla-io/argilla/pull/5685)) + ## [2.5.0](https://github.com/argilla-io/argilla/compare/v2.4.1...v2.5.0) ### Added diff --git a/argilla-server/src/argilla_server/api/handlers/v1/datasets/records.py b/argilla-server/src/argilla_server/api/handlers/v1/datasets/records.py index 90baf9d85a..104a65f6fe 100644 --- a/argilla-server/src/argilla_server/api/handlers/v1/datasets/records.py +++ b/argilla-server/src/argilla_server/api/handlers/v1/datasets/records.py @@ -264,7 +264,7 @@ async def delete_dataset_records( if num_records > DELETE_DATASET_RECORDS_LIMIT: raise UnprocessableEntityError(f"Cannot delete more than {DELETE_DATASET_RECORDS_LIMIT} records at once") - await datasets.delete_records(db, search_engine, dataset, record_ids) + await records.delete_records(db, search_engine, dataset, record_ids) @router.post( diff --git a/argilla-server/src/argilla_server/api/handlers/v1/records.py b/argilla-server/src/argilla_server/api/handlers/v1/records.py index f7dd12674c..26dc431098 100644 --- a/argilla-server/src/argilla_server/api/handlers/v1/records.py +++ b/argilla-server/src/argilla_server/api/handlers/v1/records.py @@ -25,7 +25,7 @@ from argilla_server.api.schemas.v1.responses import Response, ResponseCreate from argilla_server.api.schemas.v1.suggestions import Suggestion as SuggestionSchema from argilla_server.api.schemas.v1.suggestions import SuggestionCreate, Suggestions -from argilla_server.contexts import datasets +from argilla_server.contexts import datasets, records from argilla_server.database import get_async_db from argilla_server.errors.future.base_errors import NotFoundError, UnprocessableEntityError from argilla_server.models import Dataset, Question, Record, Suggestion, User @@ -74,16 +74,21 @@ async def update_record( db, record_id, options=[ - selectinload(Record.dataset).selectinload(Dataset.questions), - selectinload(Record.dataset).selectinload(Dataset.metadata_properties), + selectinload(Record.dataset).options( + selectinload(Dataset.questions), + selectinload(Dataset.metadata_properties), + selectinload(Dataset.vectors_settings), + selectinload(Dataset.fields), + ), selectinload(Record.suggestions), + selectinload(Record.responses), selectinload(Record.vectors), ], ) await authorize(current_user, RecordPolicy.update(record)) - return await datasets.update_record(db, search_engine, record, record_update) + return await records.update_record(db, search_engine, record, record_update) @router.post("/records/{record_id}/responses", status_code=status.HTTP_201_CREATED, response_model=Response) @@ -233,4 +238,4 @@ async def delete_record( await authorize(current_user, RecordPolicy.delete(record)) - return await datasets.delete_record(db, search_engine, record) + return await records.delete_record(db, search_engine, record) diff --git a/argilla-server/src/argilla_server/api/schemas/v1/records.py b/argilla-server/src/argilla_server/api/schemas/v1/records.py index a6d1dade7b..41ade95a8b 100644 --- a/argilla-server/src/argilla_server/api/schemas/v1/records.py +++ b/argilla-server/src/argilla_server/api/schemas/v1/records.py @@ -25,8 +25,6 @@ BaseModel, Field, StrictStr, - root_validator, - validator, ValidationError, ConfigDict, model_validator, @@ -183,17 +181,12 @@ def prevent_nan_values(cls, metadata: Optional[Dict[str, Any]]) -> Optional[Dict class RecordUpdate(UpdateSchema): - metadata_: Optional[Dict[str, Any]] = Field(None, alias="metadata") + fields: Optional[Dict[str, FieldValueCreate]] = None + metadata: Optional[Dict[str, Any]] = None suggestions: Optional[List[SuggestionCreate]] = None vectors: Optional[Dict[str, List[float]]] = None - @property - def metadata(self) -> Optional[Dict[str, Any]]: - # Align with the RecordCreate model. Both should have the same name for the metadata field. - # TODO(@frascuchon): This will be properly adapted once the bulk records refactor is completed. - return self.metadata_ - - @field_validator("metadata_") + @field_validator("metadata") @classmethod def prevent_nan_values(cls, metadata: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: if metadata is None: @@ -205,15 +198,20 @@ def prevent_nan_values(cls, metadata: Optional[Dict[str, Any]]) -> Optional[Dict return {k: v for k, v in metadata.items() if v == v} # By definition, NaN != NaN + def is_set(self, attribute: str) -> bool: + return attribute in self.model_fields_set -class RecordUpdateWithId(RecordUpdate): - id: UUID + def has_changes(self) -> bool: + return self.model_dump(exclude_unset=True) != {} class RecordUpsert(RecordCreate): id: Optional[UUID] = None fields: Optional[Dict[str, FieldValueCreate]] = None + def is_set(self, attribute: str) -> bool: + return attribute in self.model_fields_set + class RecordIncludeParam(BaseModel): relationships: Optional[List[RecordInclude]] = Field(None, alias="keys") @@ -278,13 +276,6 @@ class RecordsCreate(BaseModel): items: List[RecordCreate] = Field(..., min_length=RECORDS_CREATE_MIN_ITEMS, max_length=RECORDS_CREATE_MAX_ITEMS) -class RecordsUpdate(BaseModel): - # TODO: review this definition and align to create model - items: List[RecordUpdateWithId] = Field( - ..., min_length=RECORDS_UPDATE_MIN_ITEMS, max_length=RECORDS_UPDATE_MAX_ITEMS - ) - - class MetadataParsedQueryParam: def __init__(self, string: str): k, *v = string.split(":", maxsplit=1) diff --git a/argilla-server/src/argilla_server/api/schemas/v1/records_bulk.py b/argilla-server/src/argilla_server/api/schemas/v1/records_bulk.py index c9a945df4b..b225dca075 100644 --- a/argilla-server/src/argilla_server/api/schemas/v1/records_bulk.py +++ b/argilla-server/src/argilla_server/api/schemas/v1/records_bulk.py @@ -30,7 +30,7 @@ class RecordsBulk(BaseModel): items: List[Record] -class RecordsBulkWithUpdateInfo(RecordsBulk): +class RecordsBulkWithUpdatedItemIds(RecordsBulk): updated_item_ids: List[UUID] diff --git a/argilla-server/src/argilla_server/bulk/records_bulk.py b/argilla-server/src/argilla_server/bulk/records_bulk.py index 8a53b3114a..6e281c09e7 100644 --- a/argilla-server/src/argilla_server/bulk/records_bulk.py +++ b/argilla-server/src/argilla_server/bulk/records_bulk.py @@ -16,6 +16,7 @@ from typing import Dict, List, Sequence, Tuple, Union from uuid import UUID +from datetime import UTC from fastapi.encoders import jsonable_encoder from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -26,7 +27,7 @@ RecordsBulk, RecordsBulkCreate, RecordsBulkUpsert, - RecordsBulkWithUpdateInfo, + RecordsBulkWithUpdatedItemIds, ) from argilla_server.api.schemas.v1.responses import UserResponseCreate from argilla_server.api.schemas.v1.suggestions import SuggestionCreate @@ -39,7 +40,7 @@ fetch_records_by_ids_as_dict, ) from argilla_server.errors.future import UnprocessableEntityError -from argilla_server.models import Dataset, Record, Response, Suggestion, Vector, VectorSettings +from argilla_server.models import Dataset, Record, Response, Suggestion, Vector from argilla_server.search_engine import SearchEngine from argilla_server.validators.records import RecordsBulkCreateValidator, RecordUpsertValidator @@ -154,15 +155,11 @@ async def _upsert_records_vectors( autocommit=False, ) - @classmethod - def _metadata_is_set(cls, record_create: RecordCreate) -> bool: - return "metadata" in record_create.model_fields_set - class UpsertRecordsBulk(CreateRecordsBulk): async def upsert_records_bulk( self, dataset: Dataset, bulk_upsert: RecordsBulkUpsert, raise_on_error: bool = True - ) -> RecordsBulkWithUpdateInfo: + ) -> RecordsBulkWithUpdatedItemIds: found_records = await self._fetch_existing_dataset_records(dataset, bulk_upsert.items) records = [] @@ -185,9 +182,14 @@ async def upsert_records_bulk( external_id=record_upsert.external_id, dataset_id=dataset.id, ) - elif self._metadata_is_set(record_upsert): - record.metadata_ = record_upsert.metadata - record.updated_at = datetime.utcnow() + else: + if record_upsert.is_set("metadata"): + record.metadata_ = record_upsert.metadata + if record_upsert.is_set("fields"): + record.fields = jsonable_encoder(record_upsert.fields) + + if self._db.is_modified(record): + record.updated_at = datetime.now(UTC) records.append(record) @@ -203,7 +205,7 @@ async def upsert_records_bulk( await self._notify_upsert_record_events(records) - return RecordsBulkWithUpdateInfo( + return RecordsBulkWithUpdatedItemIds( items=records, updated_item_ids=[record.id for record in found_records.values()], ) diff --git a/argilla-server/src/argilla_server/contexts/datasets.py b/argilla-server/src/argilla_server/contexts/datasets.py index 23cb39c0f8..782010bdd2 100644 --- a/argilla-server/src/argilla_server/contexts/datasets.py +++ b/argilla-server/src/argilla_server/contexts/datasets.py @@ -12,22 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy from collections import defaultdict from datetime import datetime from typing import ( TYPE_CHECKING, - Any, - Callable, - Dict, Iterable, List, - Literal, Optional, Sequence, - Set, - Tuple, - TypeVar, Union, ) from uuid import UUID @@ -41,9 +33,7 @@ from argilla_server.api.schemas.v1.fields import FieldCreate from argilla_server.api.schemas.v1.metadata_properties import MetadataPropertyCreate, MetadataPropertyUpdate from argilla_server.api.schemas.v1.records import ( - RecordCreate, RecordIncludeParam, - RecordUpdateWithId, ) from argilla_server.api.schemas.v1.responses import ( ResponseCreate, @@ -101,7 +91,6 @@ if TYPE_CHECKING: from argilla_server.api.schemas.v1.fields import FieldUpdate - from argilla_server.api.schemas.v1.records import RecordUpdate from argilla_server.api.schemas.v1.suggestions import SuggestionCreate from argilla_server.api.schemas.v1.vector_settings import VectorSettingsUpdate @@ -333,6 +322,7 @@ async def create_vector_settings( return vector_settings +# TODO: Move this function to the records.py context async def get_records_by_ids( db: AsyncSession, records_ids: Iterable[UUID], @@ -462,103 +452,6 @@ async def get_dataset_users_progress(db: AsyncSession, dataset: Dataset) -> List return [{"username": username, **progress} for username, progress in annotators_progress.items()] -_EXTRA_METADATA_FLAG = "extra" - - -async def _validate_metadata( - db: AsyncSession, - dataset: Dataset, - metadata: Dict[str, Any], - metadata_properties: Optional[Dict[str, Union[MetadataProperty, Literal["extra"]]]] = None, -) -> Dict[str, Union[MetadataProperty, Literal["extra"]]]: - if metadata_properties is None: - metadata_properties = {} - - for name, value in metadata.items(): - metadata_property = metadata_properties.get(name) - - if metadata_property is None: - metadata_property = await MetadataProperty.get_by(db, name=name, dataset_id=dataset.id) - - # If metadata property does not exists but extra metadata is allowed, then we set a flag value to - # avoid querying the database again - if metadata_property is None and dataset.allow_extra_metadata: - metadata_property = _EXTRA_METADATA_FLAG - metadata_properties[name] = metadata_property - elif metadata_property is not None: - metadata_properties[name] = metadata_property - else: - raise ValueError( - f"'{name}' metadata property does not exists for dataset '{dataset.id}' and extra metadata is" - " not allowed for this dataset" - ) - - # If metadata property is not found and extra metadata is allowed, then we skip the value validation - if metadata_property == _EXTRA_METADATA_FLAG: - continue - - try: - if value is not None: - metadata_property.parsed_settings.check_metadata(value) - except (UnprocessableEntityError, ValueError) as e: - raise UnprocessableEntityError(f"'{name}' metadata property validation failed because {e}") from e - - return metadata_properties - - -async def validate_user_exists(db: AsyncSession, user_id: UUID, users_ids: Optional[Set[UUID]]) -> Set[UUID]: - if not users_ids: - users_ids = set() - - if user_id not in users_ids: - if not await accounts.user_exists(db, user_id): - raise UnprocessableEntityError(f"user_id={str(user_id)} does not exist") - - users_ids.add(user_id) - - return users_ids - - -async def _validate_vector( - db: AsyncSession, - dataset_id: UUID, - vector_name: str, - vector_value: List[float], - vectors_settings: Optional[Dict[str, VectorSettingsSchema]] = None, -) -> Dict[str, VectorSettingsSchema]: - if vectors_settings is None: - vectors_settings = {} - - vector_settings = vectors_settings.get(vector_name, None) - if not vector_settings: - vector_settings = await VectorSettings.get_by(db, name=vector_name, dataset_id=dataset_id) - if not vector_settings: - raise UnprocessableEntityError( - f"vector with name={str(vector_name)} does not exist for dataset_id={str(dataset_id)}" - ) - - vector_settings = VectorSettingsSchema.model_validate(vector_settings) - vectors_settings[vector_name] = vector_settings - - vector_settings.check_vector(vector_value) - - return vectors_settings - - -async def _build_record( - db: AsyncSession, dataset: Dataset, record_create: RecordCreate, caches: Dict[str, Any] -) -> Record: - _validate_record_fields(dataset, fields=record_create.fields) - await _validate_record_metadata(db, dataset, record_create.metadata, caches["metadata_properties_cache"]) - - return Record( - fields=jsonable_encoder(record_create.fields), - metadata_=record_create.metadata, - external_id=record_create.external_id, - dataset=dataset, - ) - - async def _load_users_from_responses(responses: Union[Response, Iterable[Response]]) -> None: if isinstance(responses, Response): responses = [responses] @@ -569,162 +462,6 @@ async def _load_users_from_responses(responses: Union[Response, Iterable[Respons await response.awaitable_attrs.user -async def _validate_record_metadata( - db: AsyncSession, - dataset: Dataset, - metadata: Optional[Dict[str, Any]] = None, - cache: Dict[str, Union[MetadataProperty, Literal["extra"]]] = {}, -) -> Dict[str, Union[MetadataProperty, Literal["extra"]]]: - """Validate metadata for a record.""" - if not metadata: - return cache - - try: - cache = await _validate_metadata(db, dataset=dataset, metadata=metadata, metadata_properties=cache) - return cache - except (UnprocessableEntityError, ValueError) as e: - raise UnprocessableEntityError(f"metadata is not valid: {e}") from e - - -async def _build_record_suggestions( - db: AsyncSession, - record: Record, - suggestions_create: Optional[List["SuggestionCreate"]], - questions_cache: Optional[Dict[UUID, Question]] = None, -) -> List[Suggestion]: - """Create suggestions for a record.""" - if not suggestions_create: - return [] - - suggestions = [] - for suggestion_create in suggestions_create: - try: - if not questions_cache: - questions_cache = {} - - question = questions_cache.get(suggestion_create.question_id, None) - if not question: - question = await Question.get( - db, suggestion_create.question_id, options=[selectinload(Question.dataset)] - ) - if not question: - raise UnprocessableEntityError(f"question_id={str(suggestion_create.question_id)} does not exist") - questions_cache[suggestion_create.question_id] = question - - SuggestionCreateValidator.validate(suggestion_create, question.parsed_settings, record) - - suggestions.append( - Suggestion( - type=suggestion_create.type, - score=suggestion_create.score, - value=jsonable_encoder(suggestion_create.value), - agent=suggestion_create.agent, - question_id=suggestion_create.question_id, - record=record, - ) - ) - - except (UnprocessableEntityError, ValueError) as e: - raise UnprocessableEntityError( - f"suggestion for question_id={suggestion_create.question_id} is not valid: {e}" - ) from e - - return suggestions - - -VectorClass = TypeVar("VectorClass") - - -async def _build_record_vectors( - db: AsyncSession, - dataset: Dataset, - vectors_dict: Dict[str, List[float]], - build_vector_func: Callable[[List[float], UUID], VectorClass], - cache: Optional[Dict[str, VectorSettingsSchema]] = None, -) -> List[VectorClass]: - """Create vectors for a record.""" - if not vectors_dict: - return [] - - vectors = [] - for vector_name, vector_value in vectors_dict.items(): - try: - cache = await _validate_vector(db, dataset.id, vector_name, vector_value, vectors_settings=cache) - vectors.append(build_vector_func(vector_value, cache[vector_name].id)) - except (UnprocessableEntityError, ValueError) as e: - raise UnprocessableEntityError(f"vector with name={vector_name} is not valid: {e}") from e - - return vectors - - -async def _exists_records_with_ids(db: AsyncSession, dataset_id: UUID, records_ids: List[UUID]) -> List[UUID]: - result = await db.execute(select(Record.id).filter(Record.dataset_id == dataset_id, Record.id.in_(records_ids))) - return result.scalars().all() - - -async def _build_record_update( - db: AsyncSession, record: Record, record_update: "RecordUpdateWithId", caches: Optional[Dict[str, Any]] = None -) -> Tuple[Dict[str, Any], Union[List[Suggestion], None], List[VectorSchema], bool, Dict[str, Any]]: - if caches is None: - caches = { - "metadata_properties": {}, - "questions": {}, - "vector_settings": {}, - } - - params = record_update.model_dump(exclude_unset=True) - needs_search_engine_update = False - suggestions = None - vectors = [] - - if "metadata_" in params: - metadata = params["metadata_"] - needs_search_engine_update = True - if metadata is not None: - caches["metadata_properties"] = await _validate_record_metadata( - db, record.dataset, metadata, caches["metadata_properties"] - ) - - if record_update.suggestions is not None: - params.pop("suggestions") - questions_ids = [suggestion.question_id for suggestion in record_update.suggestions] - if len(questions_ids) != len(set(questions_ids)): - raise UnprocessableEntityError("found duplicate suggestions question IDs") - suggestions = await _build_record_suggestions(db, record, record_update.suggestions, caches["questions"]) - - if record_update.vectors is not None: - params.pop("vectors") - vectors = await _build_record_vectors( - db, - record.dataset, - record_update.vectors, - build_vector_func=lambda value, vector_settings_id: VectorSchema( - value=value, record_id=record_update.id, vector_settings_id=vector_settings_id - ), - cache=caches["vector_settings"], - ) - needs_search_engine_update = True - - return params, suggestions, vectors, needs_search_engine_update, caches - - -async def _preload_records_relationships_before_index(db: AsyncSession, records: List[Record]) -> None: - for record in records: - await _preload_record_relationships_before_index(db, record) - - -async def _preload_record_relationships_before_index(db: AsyncSession, record: Record) -> None: - await db.execute( - select(Record) - .filter_by(id=record.id) - .options( - selectinload(Record.responses).selectinload(Response.user), - selectinload(Record.suggestions).selectinload(Suggestion.question), - selectinload(Record.vectors), - ) - ) - - async def preload_records_relationships_before_validate(db: AsyncSession, records: List[Record]) -> None: await db.execute( select(Record) @@ -735,70 +472,6 @@ async def preload_records_relationships_before_validate(db: AsyncSession, record ) -async def delete_records( - db: AsyncSession, search_engine: "SearchEngine", dataset: Dataset, records_ids: List[UUID] -) -> None: - params = [Record.id.in_(records_ids), Record.dataset_id == dataset.id] - - records = (await db.execute(select(Record).filter(*params).order_by(Record.inserted_at.asc()))).scalars().all() - - deleted_record_events_v1 = [] - for record in records: - deleted_record_events_v1.append( - await build_record_event_v1(db, RecordEvent.deleted, record), - ) - - records = await Record.delete_many(db, conditions=params) - - await search_engine.delete_records(dataset=dataset, records=records) - - for deleted_record_event_v1 in deleted_record_events_v1: - await deleted_record_event_v1.notify(db) - - -async def update_record( - db: AsyncSession, search_engine: "SearchEngine", record: Record, record_update: "RecordUpdate" -) -> Record: - params, suggestions, vectors, needs_search_engine_update, _ = await _build_record_update( - db, record, RecordUpdateWithId(id=record.id, **record_update.model_dump(by_alias=True, exclude_unset=True)) - ) - - # Remove existing suggestions - if suggestions is not None: - record.suggestions = [] - params["suggestions"] = suggestions - - async with db.begin_nested(): - record = await record.update(db, **params, replace_dict=True, autocommit=False) - - if vectors: - await Vector.upsert_many( - db, objects=vectors, constraints=[Vector.record_id, Vector.vector_settings_id], autocommit=False - ) - await db.refresh(record, attribute_names=["vectors"]) - - await db.commit() - - if needs_search_engine_update: - await _preload_record_relationships_before_index(db, record) - await search_engine.index_records(record.dataset, [record]) - - await notify_record_event_v1(db, RecordEvent.updated, record) - - return record - - -async def delete_record(db: AsyncSession, search_engine: "SearchEngine", record: Record) -> Record: - deleted_record_event_v1 = await build_record_event_v1(db, RecordEvent.deleted, record) - - record = await record.delete(db) - - await search_engine.delete_records(dataset=record.dataset, records=[record]) - await deleted_record_event_v1.notify(db) - - return record - - async def create_response( db: AsyncSession, search_engine: SearchEngine, record: Record, user: User, response_create: ResponseCreate ) -> Response: @@ -919,22 +592,6 @@ async def delete_response(db: AsyncSession, search_engine: SearchEngine, respons return response -def _validate_record_fields(dataset: Dataset, fields: Dict[str, Any]): - fields_copy = copy.copy(fields or {}) - for field in dataset.fields: - if field.required and not (field.name in fields_copy and fields_copy.get(field.name) is not None): - raise UnprocessableEntityError(f"missing required value for field: {field.name!r}") - - value = fields_copy.pop(field.name, None) - if value and not isinstance(value, str): - raise UnprocessableEntityError( - f"wrong value found for field {field.name!r}. Expected {str.__name__!r}, found {type(value).__name__!r}" - ) - - if fields_copy: - raise UnprocessableEntityError(f"found fields values for non configured fields: {list(fields_copy.keys())!r}") - - async def _preload_suggestion_relationships_before_index(db: AsyncSession, suggestion: Suggestion) -> None: await db.execute( select(Suggestion) diff --git a/argilla-server/src/argilla_server/contexts/records.py b/argilla-server/src/argilla_server/contexts/records.py index d49ca5e9bf..1dccaca1af 100644 --- a/argilla-server/src/argilla_server/contexts/records.py +++ b/argilla-server/src/argilla_server/contexts/records.py @@ -12,15 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime from typing import Dict, Sequence, Union, List, Tuple, Optional from uuid import UUID +from fastapi.encoders import jsonable_encoder from sqlalchemy import select, and_, func, Select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload, contains_eager -from argilla_server.database import get_async_db -from argilla_server.models import Dataset, Record, VectorSettings, Vector +from argilla_server.api.schemas.v1.records import RecordUpdate +from argilla_server.api.schemas.v1.vectors import Vector as VectorSchema + +from argilla_server.models import Dataset, Record, VectorSettings, Vector, Response, Suggestion +from argilla_server.search_engine import SearchEngine +from argilla_server.validators.records import RecordUpdateValidator +from argilla_server.webhooks.v1.enums import RecordEvent +from argilla_server.webhooks.v1.records import ( + build_record_event as build_record_event_v1, + notify_record_event as notify_record_event_v1, +) async def list_dataset_records( @@ -32,7 +43,7 @@ async def list_dataset_records( with_suggestions: bool = False, with_vectors: Union[bool, List[str]] = False, ) -> Tuple[Sequence[Record], int]: - query = _record_by_dataset_id_query( + query = _build_list_records_query( dataset_id=dataset_id, offset=offset, limit=limit, @@ -80,7 +91,7 @@ async def fetch_records_by_external_ids_as_dict( return {record.external_id: record for record in records_by_external_ids} -def _record_by_dataset_id_query( +def _build_list_records_query( dataset_id, offset: Optional[int] = None, limit: Optional[int] = None, @@ -113,3 +124,111 @@ def _record_by_dataset_id_query( query = query.limit(limit) return query.order_by(Record.inserted_at) + + +async def _preload_record_relationships_before_index(db: AsyncSession, record: Record) -> None: + await db.execute( + select(Record) + .filter_by(id=record.id) + .options( + selectinload(Record.responses).selectinload(Response.user), + selectinload(Record.suggestions).selectinload(Suggestion.question), + selectinload(Record.vectors), + ) + ) + + +async def update_record( + db: AsyncSession, search_engine: "SearchEngine", record: Record, record_update: "RecordUpdate" +) -> Record: + if not record_update.has_changes(): + return record + + dataset = record.dataset + + await RecordUpdateValidator.validate(record_update, dataset, record) + + if record_update.is_set("fields"): + record.fields = record_update.fields + + if record_update.is_set("metadata"): + record.metadata_ = record_update.metadata + + if record_update.is_set("suggestions"): + # Delete all suggestions and replace them with the new ones + await Suggestion.delete_many(db, [Suggestion.record_id == record.id], autocommit=False) + await db.refresh(record, attribute_names=["suggestions"]) + + record.suggestions = [ + Suggestion( + type=suggestion.type, + score=suggestion.score, + value=jsonable_encoder(suggestion.value), + agent=suggestion.agent, + question_id=suggestion.question_id, + record_id=record.id, + ) + for suggestion in record_update.suggestions + ] + + if record_update.vectors: + await Vector.upsert_many( + db, + objects=[ + VectorSchema( + record_id=record.id, + vector_settings_id=dataset.vector_settings_by_name(name).id, + value=value, + ) + for name, value in record_update.vectors.items() + ], + constraints=[Vector.record_id, Vector.vector_settings_id], + autocommit=False, + ) + await db.refresh(record, attribute_names=["vectors"]) + + record.updated_at = datetime.utcnow() + await record.save(db, autocommit=True) + + await _preload_record_relationships_before_index(db, record) + await search_engine.index_records(record.dataset, [record]) + + await notify_record_event_v1(db, RecordEvent.updated, record) + + return record + + +async def delete_record(db: AsyncSession, search_engine: "SearchEngine", record: Record) -> Record: + deleted_record_event_v1 = await build_record_event_v1(db, RecordEvent.deleted, record) + record = await record.delete(db=db, autocommit=True) + + await search_engine.delete_records(dataset=record.dataset, records=[record]) + + await deleted_record_event_v1.notify(db) + + return record + + +async def delete_records( + db: AsyncSession, search_engine: "SearchEngine", dataset: Dataset, records_ids: List[UUID] +) -> None: + params = [Record.id.in_(records_ids), Record.dataset_id == dataset.id] + + records = (await db.execute(select(Record).filter(*params).order_by(Record.inserted_at.asc()))).scalars().all() + + deleted_record_events_v1 = [] + for record in records: + deleted_record_events_v1.append( + await build_record_event_v1(db, RecordEvent.deleted, record), + ) + + records = await Record.delete_many( + db, + conditions=params, + autocommit=True, + ) + + await search_engine.delete_records(dataset=dataset, records=records) + + for deleted_record_event_v1 in deleted_record_events_v1: + await deleted_record_event_v1.notify(db) diff --git a/argilla-server/src/argilla_server/validators/records.py b/argilla-server/src/argilla_server/validators/records.py index 66d4a5b129..2f6e23109a 100644 --- a/argilla-server/src/argilla_server/validators/records.py +++ b/argilla-server/src/argilla_server/validators/records.py @@ -22,7 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from argilla_server.api.schemas.v1.chat import ChatFieldValue -from argilla_server.api.schemas.v1.records import RecordCreate, RecordUpsert +from argilla_server.api.schemas.v1.records import RecordCreate, RecordUpsert, RecordUpdate from argilla_server.api.schemas.v1.records_bulk import RecordsBulkCreate from argilla_server.api.schemas.v1.responses import UserResponseCreate from argilla_server.api.schemas.v1.suggestions import SuggestionCreate @@ -266,18 +266,32 @@ async def validate(cls, record_create: RecordCreate, dataset: Dataset) -> None: await cls._validate_responses(record_create.responses, dataset, record=record) +class RecordUpdateValidator(RecordValidatorBase): + @classmethod + async def validate(cls, record_update: RecordUpdate, dataset: Dataset, record: Record) -> None: + if record_update.is_set("fields"): + cls._validate_fields(record_update.fields, dataset) + + cls._validate_metadata(record_update.metadata, dataset) + cls._validate_vectors(record_update.vectors, dataset) + cls._validate_suggestions(record_update.suggestions, dataset, record=record) + + class RecordUpsertValidator(RecordValidatorBase): @classmethod async def validate(cls, record_upsert: RecordUpsert, dataset: Dataset, record: Optional[Record]) -> None: if record is None: - cls._validate_fields(record_upsert.fields, dataset) - record = Record(fields=record_upsert.fields, dataset=dataset) + return await RecordCreateValidator.validate(record_upsert, dataset) + + else: + if record_upsert.is_set("fields"): + cls._validate_fields(record_upsert.fields, dataset) - cls._validate_metadata(record_upsert.metadata, dataset) - cls._validate_vectors(record_upsert.vectors, dataset) + cls._validate_metadata(record_upsert.metadata, dataset) + cls._validate_vectors(record_upsert.vectors, dataset) + cls._validate_suggestions(record_upsert.suggestions, dataset, record=record) - cls._validate_suggestions(record_upsert.suggestions, dataset, record=record) - await cls._validate_responses(record_upsert.responses, dataset, record=record) + await cls._validate_responses(record_upsert.responses, dataset, record=record) class RecordsBulkCreateValidator: diff --git a/argilla-server/tests/unit/api/handlers/v1/datasets/records/records_bulk/test_upsert_dataset_records_bulk.py b/argilla-server/tests/unit/api/handlers/v1/datasets/records/records_bulk/test_upsert_dataset_records_bulk.py index 49649973f8..731a8fa947 100644 --- a/argilla-server/tests/unit/api/handlers/v1/datasets/records/records_bulk/test_upsert_dataset_records_bulk.py +++ b/argilla-server/tests/unit/api/handlers/v1/datasets/records/records_bulk/test_upsert_dataset_records_bulk.py @@ -93,7 +93,6 @@ async def test_upsert_dataset_records_with_empty_fields_updating_record( }, { "id": str(record.id), - "fields": {}, }, ], }, @@ -101,8 +100,88 @@ async def test_upsert_dataset_records_with_empty_fields_updating_record( assert response.status_code == 200 - assert record.fields == {"text-field": "value"} assert (await db.execute(select(func.count(Record.id)))).scalar_one() == 2 + assert record.fields == {"text-field": "value"} + + async def test_upsert_dataset_records_bulk_update_record_fields( + self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict + ): + dataset = await DatasetFactory.create(status=DatasetStatus.ready) + + await TextFieldFactory.create(name="text-field", dataset=dataset) + + record = await RecordFactory.create(fields={"text-field": "value"}, dataset=dataset) + + response = await async_client.put( + self.url(dataset.id), + headers=owner_auth_header, + json={ + "items": [ + { + "id": str(record.id), + "fields": { + "text-field": "New value", + }, + }, + ], + }, + ) + + assert response.status_code == 200 + assert (await db.execute(select(func.count(Record.id)))).scalar_one() == 1 + assert record.fields == {"text-field": "New value"} + + async def test_upsert_dataset_records_bulk_update_record_fields_with_empty_dict( + self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict + ): + dataset = await DatasetFactory.create(status=DatasetStatus.ready) + + await TextFieldFactory.create(name="text-field", dataset=dataset) + + record = await RecordFactory.create(fields={"text-field": "value"}, dataset=dataset) + + response = await async_client.put( + self.url(dataset.id), + headers=owner_auth_header, + json={ + "items": [ + { + "id": str(record.id), + "fields": {}, + }, + ], + }, + ) + + assert response.status_code == 422 + assert response.json() == {"detail": "Record at position 0 is not valid because fields cannot be empty"} + + async def test_upsert_dataset_records_bulk_update_record_fields_with_wrong_fields( + self, db: AsyncSession, async_client: AsyncClient, owner_auth_header: dict + ): + dataset = await DatasetFactory.create(status=DatasetStatus.ready) + + await TextFieldFactory.create(name="text-field", dataset=dataset, required=True) + + record = await RecordFactory.create(fields={"text-field": "value"}, dataset=dataset) + + response = await async_client.put( + self.url(dataset.id), + headers=owner_auth_header, + json={ + "items": [ + { + "id": str(record.id), + "fields": {"text-field": None}, + }, + ], + }, + ) + + assert response.status_code == 422 + assert response.json() == { + "detail": "Record at position 0 is not valid because missing required value for field: 'text-field'" + } async def test_upsert_dataset_records_bulk_updates_records_status( self, async_client: AsyncClient, owner: User, owner_auth_header: dict diff --git a/argilla-server/tests/unit/api/handlers/v1/records/test_update_record.py b/argilla-server/tests/unit/api/handlers/v1/records/test_update_record.py index d8eba32655..9d5b75e848 100644 --- a/argilla-server/tests/unit/api/handlers/v1/records/test_update_record.py +++ b/argilla-server/tests/unit/api/handlers/v1/records/test_update_record.py @@ -40,7 +40,7 @@ async def test_update_record_enqueue_webhook_record_updated_event( response = await async_client.patch( self.url(record.id), headers=owner_auth_header, - json={}, + json={"metadata": {"new": "value"}}, ) assert response.status_code == 200 diff --git a/argilla-server/tests/unit/api/handlers/v1/test_records.py b/argilla-server/tests/unit/api/handlers/v1/test_records.py index 24fc2d1073..8675b52178 100644 --- a/argilla-server/tests/unit/api/handlers/v1/test_records.py +++ b/argilla-server/tests/unit/api/handlers/v1/test_records.py @@ -18,6 +18,7 @@ from uuid import UUID, uuid4 import pytest + from argilla_server.constants import API_KEY_HEADER_NAME from argilla_server.enums import RecordStatus, ResponseStatus from argilla_server.models import Dataset, Record, Response, Suggestion, User, UserRole @@ -44,6 +45,7 @@ VectorFactory, VectorSettingsFactory, WorkspaceFactory, + TextFieldFactory, ) if TYPE_CHECKING: @@ -316,9 +318,89 @@ async def test_update_record(self, async_client: "AsyncClient", mock_search_engi "inserted_at": record.inserted_at.isoformat(), "updated_at": record.updated_at.isoformat(), } + assert record.updated_at > record.inserted_at + + mock_search_engine.index_records.assert_called_once_with(dataset, [record]) + + async def test_update_record_fields( + self, async_client: "AsyncClient", db: "AsyncSession", mock_search_engine: SearchEngine, owner_auth_header: dict + ): + dataset = await DatasetFactory.create(status="ready") + await TextFieldFactory.create(dataset=dataset, name="text", required=True) + await TextFieldFactory.create(dataset=dataset, name="sentiment", required=False) + record = await RecordFactory.create(dataset=dataset, fields={"text": "This is a text"}) + + response = await async_client.patch( + f"/api/v1/records/{record.id}", + headers=owner_auth_header, + json={"fields": {"text": "Updated text", "sentiment": "positive"}}, + ) + + assert response.status_code == 200, response.json() + assert response.json() == { + "id": str(record.id), + "status": RecordStatus.pending, + "fields": {"text": "Updated text", "sentiment": "positive"}, + "metadata": None, + "external_id": record.external_id, + "responses": [], + "suggestions": [], + "vectors": {}, + "dataset_id": str(dataset.id), + "inserted_at": record.inserted_at.isoformat(), + "updated_at": record.updated_at.isoformat(), + } + assert record.updated_at > record.inserted_at + mock_search_engine.index_records.assert_called_once_with(dataset, [record]) + + async def test_update_record_fields_with_less_fields( + self, async_client: "AsyncClient", mock_search_engine: SearchEngine, owner_auth_header: dict + ): + dataset = await DatasetFactory.create() + await TextFieldFactory.create(dataset=dataset, name="text", required=True) + await TextFieldFactory.create(dataset=dataset, name="sentiment", required=False) + record = await RecordFactory.create(dataset=dataset, fields={"text": "This is a text", "sentiment": "neutral"}) + + response = await async_client.patch( + f"/api/v1/records/{record.id}", + headers=owner_auth_header, + json={"fields": {"text": "Updated text"}}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": str(record.id), + "status": RecordStatus.pending, + "fields": {"text": "Updated text"}, + "metadata": None, + "external_id": record.external_id, + "responses": [], + "suggestions": [], + "vectors": {}, + "dataset_id": str(dataset.id), + "inserted_at": record.inserted_at.isoformat(), + "updated_at": record.updated_at.isoformat(), + } + assert record.updated_at > record.inserted_at mock_search_engine.index_records.assert_called_once_with(dataset, [record]) + async def test_update_record_fields_with_empty_fields( + self, async_client: "AsyncClient", mock_search_engine: SearchEngine, owner_auth_header: dict + ): + dataset = await DatasetFactory.create() + record = await RecordFactory.create(dataset=dataset) + + response = await async_client.patch( + f"/api/v1/records/{record.id}", + headers=owner_auth_header, + json={"fields": {}}, + ) + + assert response.status_code == 422 + assert response.json() == {"detail": "fields cannot be empty"} + assert record.updated_at == record.inserted_at + mock_search_engine.index_records.assert_not_called() + async def test_update_record_with_null_metadata( self, async_client: "AsyncClient", mock_search_engine: SearchEngine, owner_auth_header: dict ): @@ -351,6 +433,7 @@ async def test_update_record_with_null_metadata( "inserted_at": record.inserted_at.isoformat(), "updated_at": record.updated_at.isoformat(), } + assert record.updated_at > record.inserted_at mock_search_engine.index_records.assert_called_once_with(dataset, [record]) async def test_update_record_with_no_metadata( @@ -372,13 +455,14 @@ async def test_update_record_with_no_metadata( "fields": {"text": "This is a text", "sentiment": "neutral"}, "metadata": None, "external_id": record.external_id, - "responses": None, + "responses": [], "suggestions": [], "vectors": {}, "dataset_id": str(dataset.id), "inserted_at": record.inserted_at.isoformat(), "updated_at": record.updated_at.isoformat(), } + assert record.updated_at == record.inserted_at mock_search_engine.index_records.assert_not_called() async def test_update_record_with_list_terms_metadata( @@ -414,6 +498,7 @@ async def test_update_record_with_list_terms_metadata( "inserted_at": record.inserted_at.isoformat(), "updated_at": record.updated_at.isoformat(), } + assert record.updated_at > record.inserted_at mock_search_engine.index_records.assert_called_once_with(dataset, [record]) async def test_update_record_with_no_suggestions( @@ -435,13 +520,14 @@ async def test_update_record_with_no_suggestions( "fields": {"text": "This is a text", "sentiment": "neutral"}, "metadata": None, "external_id": record.external_id, - "responses": None, + "responses": [], "suggestions": [], "vectors": {}, "dataset_id": str(record.dataset_id), "inserted_at": record.inserted_at.isoformat(), "updated_at": record.updated_at.isoformat(), } + assert record.updated_at > record.inserted_at assert (await db.execute(select(Suggestion).where(Suggestion.id == suggestion.id))).scalar_one_or_none() is None @pytest.mark.parametrize( @@ -537,7 +623,9 @@ async def test_update_record_with_invalid_suggestion(self, async_client: "AsyncC assert response.status_code == 422 assert response.json() == { - "detail": f"suggestion for question_id={question.id} is not valid: 'not a valid value' is not a valid label for label selection question.\nValid labels are: ['option1', 'option2', 'option3']" + "detail": "record does not have valid suggestions: " + "'not a valid value' is not a valid label for label selection question.\n" + "Valid labels are: ['option1', 'option2', 'option3']" } async def test_update_record_with_invalid_vector(self, async_client: "AsyncClient", owner_auth_header: dict): @@ -553,7 +641,9 @@ async def test_update_record_with_invalid_vector(self, async_client: "AsyncClien assert response.status_code == 422 assert response.json() == { - "detail": f"vector with name={vector_settings.name} is not valid: vector must have 5 elements, got 6 elements" + "detail": "record does not have valid vectors: " + f"vector value for vector name={vector_settings.name} " + f"must have {vector_settings.dimensions} elements, got 6 elements" } async def test_update_record_with_suggestion_for_nonexistent_question( @@ -576,7 +666,7 @@ async def test_update_record_with_suggestion_for_nonexistent_question( assert response.status_code == 422 assert response.json() == { - "detail": f"suggestion for question_id={question_id} is not valid: question_id={question_id} does not exist" + "detail": f"record does not have valid suggestions: question id={question_id} does not exists" } async def test_update_record_with_nonexistent_vector_settings( @@ -593,7 +683,8 @@ async def test_update_record_with_nonexistent_vector_settings( assert response.status_code == 422 assert response.json() == { - "detail": f"vector with name=i-do-not-exist is not valid: vector with name=i-do-not-exist does not exist for dataset_id={dataset.id}" + "detail": "record does not have valid vectors: vector with name=i-do-not-exist " + f"does not exist for dataset_id={dataset.id}" } async def test_update_record_with_duplicate_suggestions_question_ids( @@ -615,7 +706,9 @@ async def test_update_record_with_duplicate_suggestions_question_ids( ) assert response.status_code == 422 - assert response.json() == {"detail": "found duplicate suggestions question IDs"} + assert response.json() == { + "detail": "record does not have valid suggestions: found duplicate suggestions question IDs" + } async def test_update_record_as_admin_from_another_workspace(self, async_client: "AsyncClient"): record = await RecordFactory.create() diff --git a/argilla-server/tests/unit/conftest.py b/argilla-server/tests/unit/conftest.py index 4af66e9fb2..4b029d3f71 100644 --- a/argilla-server/tests/unit/conftest.py +++ b/argilla-server/tests/unit/conftest.py @@ -91,7 +91,6 @@ async def override_get_search_engine(): mocker.patch.object(distribution, "_get_async_db", override_get_async_db) mocker.patch.object(datasets, "get_async_db", override_get_async_db) - mocker.patch.object(records, "get_async_db", override_get_async_db) api_v1.dependency_overrides.update( { diff --git a/argilla/tests/integration/test_update_records.py b/argilla/tests/integration/test_update_records.py index 3690a3cd54..a6bc77cd13 100644 --- a/argilla/tests/integration/test_update_records.py +++ b/argilla/tests/integration/test_update_records.py @@ -45,6 +45,36 @@ def dataset(client: rg.Argilla, dataset_name: str) -> rg.Dataset: return dataset +class TestUpdateRecords: + def test_update_records_fields(self, client: rg.Argilla, dataset: rg.Dataset): + mock_data = [ + { + "text": "Hello World, how are you?", + "label": "negative", + "id": uuid.uuid4(), + }, + { + "text": "Hello World, how are you?", + "label": "negative", + "id": uuid.uuid4(), + }, + { + "text": "Hello World, how are you?", + "label": "negative", + "id": uuid.uuid4(), + }, + ] + + dataset.records.log(records=mock_data) + + updated_mock_data = [{"text": "New text", "id": r["id"]} for r in mock_data] + + dataset.records.log(records=updated_mock_data) + + for record in dataset.records(): + assert record.fields["text"] == "New text" + + class TestUpdateSuggestions: def test_update_records_suggestions_from_data(self, client: rg.Argilla, dataset: rg.Dataset): mock_data = [ From e7f46ccd8bf7c5ef7320afb2d387e2da860ce444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Pumar?= Date: Thu, 12 Dec 2024 10:19:16 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9A=91=20feat/check=20version=20(#573?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Paco Aranda --- .gitignore | 1 - argilla-frontend/dev.frontend.Dockerfile | 1 + argilla-frontend/package-lock.json | 235 +++++++++++------------ argilla-frontend/package.json | 103 +++++----- 4 files changed, 160 insertions(+), 180 deletions(-) diff --git a/.gitignore b/.gitignore index 216119fa84..5e093d53d6 100644 --- a/.gitignore +++ b/.gitignore @@ -133,7 +133,6 @@ sw.* # Ruff cache .ruff_cache/ - # App generated files src/**/server/static/ diff --git a/argilla-frontend/dev.frontend.Dockerfile b/argilla-frontend/dev.frontend.Dockerfile index d268ae7b02..ce7ea87954 100644 --- a/argilla-frontend/dev.frontend.Dockerfile +++ b/argilla-frontend/dev.frontend.Dockerfile @@ -14,6 +14,7 @@ WORKDIR /home/argilla/frontend COPY --chown=argilla:argilla dist ./dist COPY --chown=argilla:argilla .nuxt ./.nuxt COPY --chown=argilla:argilla package.json ./package.json +COPY --chown=argilla:argilla package-lock.json ./package-lock.json COPY --chown=argilla:argilla nuxt.config.ts ./nuxt.config.ts # NOTE: Right now this Docker image is using dev.argilla.io as server. diff --git a/argilla-frontend/package-lock.json b/argilla-frontend/package-lock.json index ec2b1cf9bf..cb75a08577 100644 --- a/argilla-frontend/package-lock.json +++ b/argilla-frontend/package-lock.json @@ -8,63 +8,62 @@ "name": "argilla", "version": "2.6.0dev0", "dependencies": { - "@codescouts/events": "^1.0.2", + "@codescouts/events": "1.0.10", "@nuxtjs/auth-next": "5.0.0-1613647907.37b1156", - "@nuxtjs/axios": "^5.12.5", - "@nuxtjs/composition-api": "^0.33.1", - "@nuxtjs/i18n": "^7.3.1", + "@nuxtjs/axios": "5.13.6", + "@nuxtjs/composition-api": "0.33.1", + "@nuxtjs/i18n": "7.3.1", "@nuxtjs/style-resources": "^1.0.0", "@pinia/nuxt": "^0.2.1", - "@vuex-orm/core": "^0.36.4", - "axios": "^1.4.0", - "core-js": "^3.6.5", - "dompurify": "^3.0.3", - "frontmatter-markdown-loader": "^3.7.0", - "marked": "^5.0.3", - "marked-highlight": "^2.0.1", - "marked-katex-extension": "^5.0.2", - "nuxt": "^2.15.8", - "nuxt-highlightjs": "^1.0.2", - "pinia": "^2.1.4", - "sass": "^1.49.9", - "ts-injecty": "^0.0.22", - "v-click-outside": "^3.1.2", - "vue": "^2.7.14", - "vue-demi": "^0.14.5", - "vue-svgicon": "^3.2.9", - "vue-template-compiler": "^2.6.14", - "vuedraggable": "^2.24.3", - "vuex": "^3.1.3" + "@vuex-orm/core": "0.36.4", + "axios": "1.6.8", + "core-js": "3.37.1", + "dompurify": "3.1.3", + "frontmatter-markdown-loader": "3.7.0", + "marked": "5.1.2", + "marked-highlight": "2.1.1", + "marked-katex-extension": "5.0.2", + "nuxt": "2.17.3", + "nuxt-highlightjs": "1.0.3", + "pinia": "2.1.7", + "sass": "1.77.1", + "ts-injecty": "0.0.22", + "v-click-outside": "3.2.0", + "vue": "2.7.16", + "vue-demi": "0.14.10", + "vue-svgicon": "3.3.2", + "vue-template-compiler": "2.7.16", + "vuedraggable": "2.24.3" }, "devDependencies": { - "@babel/core": "^7.22.0", - "@babel/eslint-parser": "^7.15.0", - "@babel/preset-env": "^7.15.0", - "@babel/preset-typescript": "^7.22.5", - "@codescouts/test": "^1.0.7", - "@intlify/eslint-plugin-vue-i18n": "^2.0.0", - "@nuxt/types": "^2.15.8", - "@nuxt/typescript-build": "^2.1.0", - "@nuxtjs/eslint-config-typescript": "^12.0.0", - "@playwright/test": "^1.35.1", - "@types/jest": "^27.5.2", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.60.1", - "@vue/test-utils": "~1.3.0", - "babel-core": "^7.0.0-bridge.0", - "cross-env": "^7.0.3", - "eslint": "^8.43.0", - "eslint-config-prettier": "^7.1.0", - "eslint-plugin-nuxt": "^3.1.0", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-vue": "^8.2.0", - "jest": "^27.4.5", - "jest-serializer-vue": "^2.0.2", - "jest-transform-stub": "^2.0.0", - "prettier": "^2.2.1", - "sass-loader": "^10.1.0", - "typescript": "^5.1.6", - "vue-jest": "^3.0.7" + "@babel/core": "7.25.2", + "@babel/eslint-parser": "7.24.8", + "@babel/preset-env": "7.24.5", + "@babel/preset-typescript": "7.24.1", + "@codescouts/test": "1.0.8", + "@intlify/eslint-plugin-vue-i18n": "2.0.0", + "@nuxt/types": "2.18.1", + "@nuxt/typescript-build": "2.1.0", + "@nuxtjs/eslint-config-typescript": "12.1.0", + "@playwright/test": "1.49.0", + "@types/jest": "27.5.2", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "@vue/test-utils": "1.3.6", + "babel-core": "7.0.0-bridge.0", + "cross-env": "7.0.3", + "eslint": "8.57.1", + "eslint-config-prettier": "7.2.0", + "eslint-plugin-nuxt": "3.2.0", + "eslint-plugin-prettier": "3.4.1", + "eslint-plugin-vue": "8.7.1", + "jest": "27.5.1", + "jest-serializer-vue": "2.0.2", + "jest-transform-stub": "2.0.0", + "prettier": "2.8.8", + "sass-loader": "10.5.2", + "typescript": "5.7.2", + "vue-jest": "3.0.7" } }, "node_modules/@ampproject/remapping": { @@ -134,9 +133,9 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/eslint-parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.5.tgz", - "integrity": "sha512-gsUcqS/fPlgAw1kOtpss7uhY6E9SFFANQ6EFX5GTvzUwaV0+sGaZWk6xq22MOdeT9wfxyokW3ceCUvOiRtZciQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.8.tgz", + "integrity": "sha512-nYAikI4XTGokU2QX7Jx+v4rxZKhKivaQaREZjuW3mrJrbdWJ5yUfohnoUULge+zEEaKjPYNxhoRgUKktjXtbwA==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -1867,9 +1866,9 @@ "dev": true }, "node_modules/@codescouts/events": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@codescouts/events/-/events-1.0.2.tgz", - "integrity": "sha512-eDeRuYvX/ykJhtemR/q+k01WkHK8i0jTwAxJX4pgBs2XGZ2Jlf5av6mRSh34gv/jkrf3Z0ZkmjgqrcWsraQx/w==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@codescouts/events/-/events-1.0.10.tgz", + "integrity": "sha512-nIfJzZuLnuCCQ5o4/pon4Cirwxicw4VQaGAfPWbQCx2v3O9BcrQRhBbdJdbaiMoW+WKUA3F1nWVVGKARI9Uu1g==", "dependencies": { "typescript": "^4.6.4" } @@ -4887,9 +4886,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4901,12 +4900,13 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -4931,6 +4931,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@intlify/core-base": { @@ -7576,9 +7577,9 @@ } }, "node_modules/@nuxt/types": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/@nuxt/types/-/types-2.17.3.tgz", - "integrity": "sha512-IXM+DwDrBj96v2O+oQrqA1vhQMVnBBcU7lTb+Xhnl6StL2PU6hxcx2czUWS8p2K6B6xvOHu+Sda7rCOKP60j2g==", + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/@nuxt/types/-/types-2.18.1.tgz", + "integrity": "sha512-PpReoV9oHCnSpB9WqemTUWmlH1kqFHC3Xe5LH904VvCl/3xLO2nGYcrHeZCMV5hXNWsDUyqDnd/2cQHmeqj5lA==", "dev": true, "dependencies": { "@types/babel__core": "7.20.5", @@ -7586,12 +7587,12 @@ "@types/connect": "3.4.38", "@types/etag": "1.8.3", "@types/file-loader": "5.0.4", - "@types/html-minifier": "4.0.5", + "@types/html-minifier-terser": "7.0.2", "@types/less": "3.0.6", "@types/node": "^16", "@types/optimize-css-assets-webpack-plugin": "5.0.8", "@types/pug": "2.0.10", - "@types/serve-static": "1.15.5", + "@types/serve-static": "1.15.7", "@types/terser-webpack-plugin": "4.2.1", "@types/webpack": "^4.41.38", "@types/webpack-bundle-analyzer": "3.9.5", @@ -7601,6 +7602,12 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/@nuxt/types/node_modules/@types/html-minifier-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-7.0.2.tgz", + "integrity": "sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==", + "dev": true + }, "node_modules/@nuxt/typescript-build": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nuxt/typescript-build/-/typescript-build-2.1.0.tgz", @@ -8432,18 +8439,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz", - "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", "dev": true, "dependencies": { - "playwright": "1.44.0" + "playwright": "1.49.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@polka/url": { @@ -8767,16 +8774,6 @@ "@types/node": "*" } }, - "node_modules/@types/clean-css": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", - "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "source-map": "^0.6.0" - } - }, "node_modules/@types/compression": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", @@ -8868,17 +8865,6 @@ "@types/node": "*" } }, - "node_modules/@types/html-minifier": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-4.0.5.tgz", - "integrity": "sha512-LfE7f7MFd+YUfZnlBz8W43P4NgSObWiqyKapANsWCj63Aqeqli8/9gVsGP4CwC8jPpTTYlTopKCk9rJSuht/ew==", - "dev": true, - "dependencies": { - "@types/clean-css": "*", - "@types/relateurl": "*", - "@types/uglify-js": "*" - } - }, "node_modules/@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -9026,12 +9012,6 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, - "node_modules/@types/relateurl": { - "version": "0.2.33", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", - "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -9049,14 +9029,14 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, "dependencies": { "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/set-cookie-parser": { @@ -14907,16 +14887,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -23984,33 +23965,33 @@ "peer": true }, "node_modules/playwright": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", - "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", "dev": true, "dependencies": { - "playwright-core": "1.44.0" + "playwright-core": "1.49.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz", - "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/playwright/node_modules/fsevents": { @@ -28561,9 +28542,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -29161,9 +29142,9 @@ "integrity": "sha512-vKl1skEKn8EK9f8P2ZzhRnuaRHLHrlt1sbRmazlvsx6EiC3A8oWF8YCBrMJzoN+W3OnElwIGbVjsx6/xelY1AA==" }, "node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", diff --git a/argilla-frontend/package.json b/argilla-frontend/package.json index 0a1325ddfc..2cd8b1211f 100644 --- a/argilla-frontend/package.json +++ b/argilla-frontend/package.json @@ -21,63 +21,62 @@ "test:watch": "jest --colors --watchAll" }, "dependencies": { - "@codescouts/events": "^1.0.2", + "@codescouts/events": "1.0.10", "@nuxtjs/auth-next": "5.0.0-1613647907.37b1156", - "@nuxtjs/axios": "^5.12.5", - "@nuxtjs/composition-api": "^0.33.1", - "@nuxtjs/i18n": "^7.3.1", + "@nuxtjs/axios": "5.13.6", + "@nuxtjs/composition-api": "0.33.1", + "@nuxtjs/i18n": "7.3.1", "@nuxtjs/style-resources": "^1.0.0", "@pinia/nuxt": "^0.2.1", - "@vuex-orm/core": "^0.36.4", - "axios": "^1.4.0", - "core-js": "^3.6.5", - "dompurify": "^3.0.3", - "frontmatter-markdown-loader": "^3.7.0", - "marked": "^5.0.3", - "marked-highlight": "^2.0.1", - "marked-katex-extension": "^5.0.2", - "nuxt": "^2.15.8", - "nuxt-highlightjs": "^1.0.2", - "pinia": "^2.1.4", - "sass": "^1.49.9", - "ts-injecty": "^0.0.22", - "v-click-outside": "^3.1.2", - "vue": "^2.7.14", - "vue-demi": "^0.14.5", - "vue-svgicon": "^3.2.9", - "vue-template-compiler": "^2.6.14", - "vuedraggable": "^2.24.3", - "vuex": "^3.1.3" + "@vuex-orm/core": "0.36.4", + "axios": "1.6.8", + "core-js": "3.37.1", + "dompurify": "3.1.3", + "frontmatter-markdown-loader": "3.7.0", + "marked": "5.1.2", + "marked-highlight": "2.1.1", + "marked-katex-extension": "5.0.2", + "nuxt": "2.17.3", + "nuxt-highlightjs": "1.0.3", + "pinia": "2.1.7", + "sass": "1.77.1", + "ts-injecty": "0.0.22", + "v-click-outside": "3.2.0", + "vue": "2.7.16", + "vue-demi": "0.14.10", + "vue-svgicon": "3.3.2", + "vue-template-compiler": "2.7.16", + "vuedraggable": "2.24.3" }, "devDependencies": { - "@babel/core": "^7.22.0", - "@babel/eslint-parser": "^7.15.0", - "@babel/preset-env": "^7.15.0", - "@babel/preset-typescript": "^7.22.5", - "@codescouts/test": "^1.0.7", - "@intlify/eslint-plugin-vue-i18n": "^2.0.0", - "@nuxt/types": "^2.15.8", - "@nuxt/typescript-build": "^2.1.0", - "@nuxtjs/eslint-config-typescript": "^12.0.0", - "@playwright/test": "^1.35.1", - "@types/jest": "^27.5.2", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.60.1", - "@vue/test-utils": "~1.3.0", - "babel-core": "^7.0.0-bridge.0", - "cross-env": "^7.0.3", - "eslint": "^8.43.0", - "eslint-config-prettier": "^7.1.0", - "eslint-plugin-nuxt": "^3.1.0", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-vue": "^8.2.0", - "jest": "^27.4.5", - "jest-serializer-vue": "^2.0.2", - "jest-transform-stub": "^2.0.0", + "@babel/core": "7.25.2", + "@babel/eslint-parser": "7.24.8", + "@babel/preset-env": "7.24.5", + "@babel/preset-typescript": "7.24.1", + "@codescouts/test": "1.0.8", + "@intlify/eslint-plugin-vue-i18n": "2.0.0", + "@nuxt/types": "2.18.1", + "@nuxt/typescript-build": "2.1.0", + "@nuxtjs/eslint-config-typescript": "12.1.0", + "@playwright/test": "1.49.0", + "@types/jest": "27.5.2", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "@vue/test-utils": "1.3.6", + "babel-core": "7.0.0-bridge.0", + "cross-env": "7.0.3", + "eslint": "8.57.1", + "eslint-config-prettier": "7.2.0", + "eslint-plugin-nuxt": "3.2.0", + "eslint-plugin-prettier": "3.4.1", + "eslint-plugin-vue": "8.7.1", + "jest": "27.5.1", + "jest-serializer-vue": "2.0.2", + "jest-transform-stub": "2.0.0", "nuxt-compress": "5.0.0", - "prettier": "^2.2.1", - "sass-loader": "^10.1.0", - "typescript": "^5.1.6", - "vue-jest": "^3.0.7" + "prettier": "2.8.8", + "sass-loader": "10.5.2", + "typescript": "5.7.2", + "vue-jest": "3.0.7" } }