Merge branch 'main' into features/ts-support-2

This commit is contained in:
Convly 2023-05-30 11:16:12 +02:00
commit c7576cde72
26 changed files with 1400 additions and 97 deletions

View File

@ -0,0 +1,343 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { createTestBuilder } = require('api-tests/builder');
const { createStrapiInstance } = require('api-tests/strapi');
const { createContentAPIRequest } = require('api-tests/request');
const builder = createTestBuilder();
let strapi;
let rq;
const modelUID = 'api::model.model';
const componentUID = 'default.component';
const models = {
[modelUID]: {
displayName: 'Model',
singularName: 'model',
pluralName: 'models',
kind: 'collectionType',
attributes: {
name: {
type: 'text',
},
media: {
type: 'media',
},
media_repeatable: {
type: 'media',
multiple: true,
},
compo_media: {
type: 'component',
component: componentUID,
},
compo_media_repeatable: {
type: 'component',
repeatable: true,
component: componentUID,
},
dynamicZone: {
type: 'dynamiczone',
components: [componentUID],
},
},
},
[componentUID]: {
displayName: 'component',
attributes: {
media_repeatable: {
type: 'media',
multiple: true,
},
media: {
type: 'media',
multiple: false,
},
},
},
};
const populate = {
media: true,
media_repeatable: true,
compo_media: {
populate: {
media: true,
media_repeatable: true,
},
},
compo_media_repeatable: {
populate: {
media: true,
media_repeatable: true,
},
},
dynamicZone: {
populate: {
media: true,
media_repeatable: true,
},
},
};
let isPrivate = true;
const mockProvider = () => ({
init() {
return {
isPrivate() {
return isPrivate;
},
getSignedUrl() {
return { url: 'signedUrl' };
},
uploadStream(file) {
file.url = 'strapi.jpg';
},
upload(file) {
file.url = 'strapi.jpg';
},
delete() {},
checkFileSize() {},
};
},
});
const createModel = async (name = 'name') => {
return strapi.entityService.create(modelUID, {
data: {
name,
media: singleMedia,
media_repeatable: repeatable,
compo_media: mediaEntry,
compo_media_repeatable: [mediaEntry, mediaEntry],
dynamicZone: [
{
__component: componentUID,
...mediaEntry,
},
],
},
populate,
});
};
const uploadImg = (fileName) => {
return rq({
method: 'POST',
url: '/upload',
formData: {
files: fs.createReadStream(path.join(__dirname, `../utils/${fileName}`)),
},
});
};
let repeatable;
let singleMedia;
let mediaEntry = {};
let model;
describe('Upload Plugin url signing', () => {
const expectMedia = (media, expectedUrl) => {
expect(media.url).toEqual(expectedUrl);
};
const expectRepeatable = (repeatable, expectedUrl) => {
for (const media of repeatable) {
expectMedia(media, expectedUrl);
}
};
const responseExpectations = (result, expectedUrl) => {
expectMedia(result.media, expectedUrl);
expectRepeatable(result.media_repeatable, expectedUrl);
expect(result.compo_media.media.url).toEqual(expectedUrl);
for (const media of result.compo_media.media_repeatable) {
expect(media.url).toEqual(expectedUrl);
}
for (const component of result.compo_media_repeatable) {
expect(component.media.url).toEqual(expectedUrl);
for (const media of component.media_repeatable) {
expect(media.url).toEqual(expectedUrl);
}
}
for (const component of result.dynamicZone) {
expect(component.media.url).toEqual(expectedUrl);
for (const media of component.media_repeatable) {
expect(media.url).toEqual(expectedUrl);
}
}
};
beforeAll(async () => {
const localProviderPath = require.resolve('@strapi/provider-upload-local');
jest.mock(localProviderPath, () => mockProvider(true));
// Create builder
await builder.addComponent(models[componentUID]).addContentType(models[modelUID]).build();
// Create api instance
strapi = await createStrapiInstance();
rq = await createContentAPIRequest({ strapi });
const imgRes = [await uploadImg('strapi.jpg'), await uploadImg('strapi.jpg')];
repeatable = imgRes.map((img) => img.body[0].id);
singleMedia = imgRes[0].body[0].id;
mediaEntry = {
media: singleMedia,
media_repeatable: repeatable,
};
model = await createModel('model1');
});
afterAll(async () => {
await strapi.destroy();
await builder.cleanup();
});
describe('Returns signed media URLs on', () => {
test('entityService.create', async () => {
let entity = await createModel();
responseExpectations(entity, 'signedUrl');
});
test('entityService.findOne', async () => {
const entity = await strapi.entityService.findOne(modelUID, model.id, {
populate,
});
responseExpectations(entity, 'signedUrl');
});
test('entityService.findMany', async () => {
const entities = await strapi.entityService.findMany(modelUID, {
populate,
});
for (const entity of entities) {
responseExpectations(entity, 'signedUrl');
}
});
test('entityService.findPage', async () => {
const entities = await strapi.entityService.findPage(modelUID, {
populate,
});
for (const entity of entities.results) {
responseExpectations(entity, 'signedUrl');
}
});
test('entityService.update', async () => {
const model = await createModel();
const entity = await strapi.entityService.update(modelUID, model.id, {
data: {
name: 'model_updated',
},
populate,
});
responseExpectations(entity, 'signedUrl');
});
test('entityService.delete', async () => {
const model = await createModel();
const entity = await strapi.entityService.delete(modelUID, model.id, {
populate,
});
responseExpectations(entity, 'signedUrl');
});
test('entityService.load', async () => {
const model = await createModel();
const media_repeatable = await strapi.entityService.load(
modelUID,
{ id: model.id },
'media_repeatable'
);
expectRepeatable(media_repeatable, 'signedUrl');
});
});
describe('Does not return signed media URLs on', () => {
beforeAll(async () => {
isPrivate = false;
});
test('entityService.create', async () => {
let entity = await createModel();
responseExpectations(entity, 'strapi.jpg');
});
test('entityService.findOne', async () => {
const entity = await strapi.entityService.findOne(modelUID, model.id, {
populate,
});
responseExpectations(entity, 'strapi.jpg');
});
test('entityService.findMany', async () => {
const entities = await strapi.entityService.findMany(modelUID, {
populate,
});
for (const entity of entities) {
responseExpectations(entity, 'strapi.jpg');
}
});
test('entityService.findPage', async () => {
const entities = await strapi.entityService.findPage(modelUID, {
populate,
});
for (const entity of entities.results) {
responseExpectations(entity, 'strapi.jpg');
}
});
test('entityService.update', async () => {
const model = await createModel();
const entity = await strapi.entityService.update(modelUID, model.id, {
data: {
name: 'model_updated',
},
populate,
});
responseExpectations(entity, 'strapi.jpg');
});
test('entityService.delete', async () => {
const model = await createModel();
const entity = await strapi.entityService.delete(modelUID, model.id, {
populate,
});
responseExpectations(entity, 'strapi.jpg');
});
test('entityService.load', async () => {
const model = await createModel();
const media_repeatable = await strapi.entityService.load(
modelUID,
{ id: model.id },
'media_repeatable'
);
expectRepeatable(media_repeatable, 'strapi.jpg');
});
});
});

View File

@ -0,0 +1,109 @@
---
title: Transactions
description: Conceptual guide to transactions in Strapi
tags:
- database
- experimental
---
:::caution
This is an experimental feature and is subject to change in future versions.
:::
An API to wrap a set of operations in a transaction that ensures the integrity of data.
## What are transactions
Transactions are a set of operations that are executed together as a single unit. If any of the operations fail, the entire transaction fails and the data is rolled back to its previous state. If all operations succeed, the transaction is committed and the data is permanently saved to the database.
## Usage
Transactions are handled by passing a handler function into `strapi.db.transaction`:
```js
await strapi.db.transaction(async ({ trx, rollback, commit, onCommit, onRollback }) => {
// It will implicitly use the transaction
await strapi.entityService.create();
await strapi.entityService.create();
});
```
After the transaction handler is executed, the transaction is committed if all operations succeed. If any of the operations throws, the transaction is rolled back and the data is restored to its previous state.
:::note
Every `strapi.entityService` or `strapi.db.query` operation performed in a transaction block will implicitly use the transaction.
:::
### Transaction handler properties
The handler function receives an object with the following properties:
| Property | Description |
| ------------ | ------------------------------------------------------------------------------------------- |
| `trx` | The transaction object. It can be used to perform knex queries within the transaction. |
| `commit` | Function to commit the transaction. |
| `rollback` | Function to rollback the transaction. |
| `onCommit` | Function to register a callback that will be executed after the transaction is committed. |
| `onRollback` | Function to register a callback that will be executed after the transaction is rolled back. |
### Nested transactions
Transactions can be nested. When a transaction is nested, the inner transaction is committed or rolled back when the outer transaction is committed or rolled back.
```js
await strapi.db.transaction(async () => {
// It will implicitly use the transaction
await strapi.entityService.create();
// Nested transactions will implicitly use the outer transaction
await strapi.db.transaction(async ({}) => {
await strapi.entityService.create();
});
});
```
### onCommit and onRollback
The `onCommit` and `onRollback` hooks can be used to execute code after the transaction is committed or rolled back.
```js
await strapi.db.transaction(async ({ onCommit, onRollback }) => {
// It will implicitly use the transaction
await strapi.entityService.create();
await strapi.entityService.create();
onCommit(() => {
// This will be executed after the transaction is committed
});
onRollback(() => {
// This will be executed after the transaction is rolled back
});
});
```
### Using knex queries
Transactions can also be used with knex queries, but in those cases `.transacting(trx)` must be explicitly called.
```js
await strapi.db.transaction(async ({ trx, rollback, commit }) => {
await knex('users').where('id', 1).update({ name: 'foo' }).transacting(trx);
});
```
## When to use transactions
Transactions should be used in cases where multiple operations should be executed together and their execution is dependent on each other. For example, when creating a new user, the user should be created in the database and a welcome email should be sent to the user. If the email fails to send, the user should not be created in the database.
## When not to use transactions
Transactions should not be used for operations that are not dependent on each other since it can result in performance penalties.
## Potential problems of transactions
Performing multiple operations within a transaction can lead to locking, which can block the execution of transactions from other processes until the original transaction is complete.
Furthermore, transactions can stall if they are not committed or rolled back appropriately.
For example, if a transaction is opened but there is a path in your code that does not close it, the transaction will be left open indefinitely and could cause instability until your server is restarted and the connection is forced to close. These issues can be difficult to debug, so use transactions with care in the cases they are necessary.

View File

@ -205,5 +205,688 @@
"content-manager.success.record.save": "حُفظ",
"notification.error": "حدث خطأ",
"notification.error.layout": "تعذّر استرداد التنسيق",
"request.error.model.unknown": "هذا النموذج غير موجود"
"request.error.model.unknown": "هذا النموذج غير موجود",
"admin.pages.MarketPlacePage.filters.categories": "فئات",
"admin.pages.MarketPlacePage.filters.collections": "المجموعات",
"admin.pages.MarketPlacePage.helmet": "السوق - الإضافات",
"admin.pages.MarketPlacePage.missingPlugin.description": "أخبرنا ما هو المكون الإضافي الذي تبحث عنه وسنعلم مطوري المكونات الإضافية في مجتمعنا في حال كانوا يبحثون عن الإلهام!",
"admin.pages.MarketPlacePage.missingPlugin.title": "هل فقدت مكونًا إضافيًا؟",
"admin.pages.MarketPlacePage.offline.subtitle": "يجب أن تكون متصلاً بالإنترنت للوصول إلى سوق سترابي",
"admin.pages.MarketPlacePage.offline.title": "انت غير متصل",
"admin.pages.MarketPlacePage.plugin.copy": "أمر نسخ التثبيت",
"admin.pages.MarketPlacePage.plugin.copy.success": "قم بتثبيت الأمر جاهزًا ليتم لصقه في الجهاز الطرفي",
"admin.pages.MarketPlacePage.plugin.downloads": "يتم تنزيل هذا المكون الإضافي {downloadsCount} أسبوعيًا",
"admin.pages.MarketPlacePage.plugin.githubStars": "تم تمييز هذا المكون الإضافي بنجمة على GitHub",
"admin.pages.MarketPlacePage.plugin.info": "يتعلم أكثر",
"admin.pages.MarketPlacePage.plugin.info.label": "تعرف على المزيد حول {pluginName}",
"admin.pages.MarketPlacePage.plugin.info.text": "أكثر",
"admin.pages.MarketPlacePage.plugin.installed": "المثبتة",
"admin.pages.MarketPlacePage.plugin.tooltip.madeByStrapi": "صنع بواسطة ستربي",
"admin.pages.MarketPlacePage.plugin.tooltip.verified": "Strapi تم التحقق من البرنامج المساعد من قبل ",
"admin.pages.MarketPlacePage.plugins": "الإضافات",
"admin.pages.MarketPlacePage.provider.downloads": "هذا الموفر لديه {downloadsCount} من التنزيلات الأسبوعية",
"admin.pages.MarketPlacePage.provider.githubStars": "{GitHub} على {starsCount} تم تميز هذا المزود ",
"admin.pages.MarketPlacePage.providers": "الموفرون",
"admin.pages.MarketPlacePage.search.clear": "مسح البحث",
"admin.pages.MarketPlacePage.search.empty": " \"{target}\" لا توجد نتيجة ل",
"admin.pages.MarketPlacePage.search.placeholder": "يبحث",
"admin.pages.MarketPlacePage.sort.alphabetical": "ترتيب ابجدي",
"admin.pages.MarketPlacePage.sort.alphabetical.selected": "فرز حسب الترتيب الأبجدي",
"admin.pages.MarketPlacePage.sort.githubStars": "GitHub عدد نجوم",
"admin.pages.MarketPlacePage.sort.githubStars.selected": "GitHub الترتيب حسب نجوم",
"admin.pages.MarketPlacePage.sort.newest": "الأحدث",
"admin.pages.MarketPlacePage.sort.newest.selected": "ترتيب حسب الأحدث",
"admin.pages.MarketPlacePage.sort.npmDownloads": "عدد التنزيلات",
"admin.pages.MarketPlacePage.sort.npmDownloads.selected": "npm فرز حسب التنزيلات ",
"admin.pages.MarketPlacePage.submit.plugin.link": "إرسال البرنامج المساعد",
"admin.pages.MarketPlacePage.submit.provider.link": "إرسال مزود",
"admin.pages.MarketPlacePage.subtitle": " Strapi احصل على المزيد من",
"admin.pages.MarketPlacePage.tab-group.label": "Strapi الإضافات ومقدمي ",
"Analytics": "تحليلات",
"anErrorOccurred": "عذرًا! هناك خطأ ما. حاول مرة اخرى.",
"app.component.CopyToClipboard.label": "نسخ إلى الحافظة",
"app.component.search.label": "{target} بحث عن",
"app.component.table.duplicate": "{target} ينسخ",
"app.component.table.edit": "{target} يحرر",
"app.component.table.read": "{target}يقرأ",
"app.component.table.select.one-entry": "{target}يختار",
"app.component.table.view": "{target} تفاصيل",
"app.components.BlockLink.blog": "مدونة",
"admin.pages.MarketPlacePage.filters.categoriesSelected": "{count, plural, =0 {No categories} واحد {# category} آخر{# categories}} المحدد",
"admin.pages.MarketPlacePage.plugin.version": " قم بتحديث إصدار الخاص بك : \"{strapiAppVersion}\" ل: \"{versionRange}\"",
"admin.pages.MarketPlacePage.plugin.version.null": "تعذر التحقق من التوافق مع إصدار Strapi الخاص بك: \"{strapiAppVersion}\"",
"app.components.BlockLink.blog.content": "اقرأ آخر الأخبار حول Strapi والنظام البيئي.",
"app.components.BlockLink.cloud": "Strapi سحاب",
"app.components.BlockLink.cloud.content": "نظام أساسي قابل للإنشاء والتعاون بشكل كامل لزيادة سرعة فريقك.",
"app.components.BlockLink.code.content": "تعلم من خلال اختبار المشاريع الحقيقية التي طورها المجتمع.",
"app.components.BlockLink.documentation.content": "اكتشف المفاهيم الأساسية والأدلة والتعليمات.",
"app.components.BlockLink.tutorial": "دروس",
"app.components.BlockLink.tutorial.content": "Strapi اتبع التعليمات خطوة بخطوة للاستخدام والتخصيص.",
"app.components.Button.confirm": "يتأكد",
"app.components.Button.reset": "إعادة ضبط",
"app.components.ConfirmDialog.title": "تأكيد",
"app.components.EmptyStateLayout.content-document": "لم يتم العثور على محتوى",
"app.components.EmptyStateLayout.content-permissions": "ليس لديك أذونات للوصول إلى هذا المحتوى",
"app.components.GuidedTour.apiTokens.create.content": "<p>قم بإنشاء رمز المصادقة هنا واسترجع المحتوى الذي أنشأته للتو.</p>",
"app.components.GuidedTour.apiTokens.create.cta.title": "API إنشاء رمز",
"app.components.GuidedTour.apiTokens.create.title": "🚀 مشاهدة المحتوى في العمل",
"app.components.GuidedTour.apiTokens.success.cta.title": "العودة الى الصفحة الرئيسية",
"app.components.GuidedTour.apiTokens.success.title": "✅ الخطوة 3: اكتمل",
"app.components.GuidedTour.CM.create.content": "<p>قم بإنشاء وإدارة كل المحتوى هنا في إدارة المحتوى.</p><p>مثال: أخذ مثال موقع المدونة إلى أبعد من ذلك ، يمكن للمرء كتابة مقال وحفظه ونشره كما يحلو له.</p><p>💡 نصيحة سريعة - لا تنس النقر على ’نشر’ على المحتوى الذي تنشئه.</p>",
"app.components.GuidedTour.CM.create.title": "⚡️ أنشئ محتوى",
"app.components.GuidedTour.CM.success.content": "<p>رائع ، خطوة أخيرة يجب أن تبدأ بها!</p><b>🚀 مشاهدة المحتوى في العمل</b>",
"app.components.GuidedTour.CM.success.cta.title": "API اختبر ملف",
"app.components.GuidedTour.CM.success.title": "✅ الخطوة 2: اكتمل",
"app.components.GuidedTour.create-content": "أنشئ محتوى",
"app.components.GuidedTour.CTB.create.content": "<p>تساعدك أنواع المجموعات على إدارة عدة إدخالات ، والأنواع الفردية مناسبة لإدارة إدخال واحد فقط. </ p> <p> على سبيل المثال: بالنسبة إلى موقع مدونة ، ستكون المقالات من نوع المجموعة بينما تكون الصفحة الرئيسية من النوع الفردي.</p>",
"app.components.GuidedTour.CTB.create.cta.title": "بناء نوع المجموعة",
"app.components.GuidedTour.CTB.create.title": "🧠 قم بإنشاء أول نوع مجموعة",
"app.components.GuidedTour.CTB.success.content": "<p>جيد!</p><b>⚡️ ما الذي تود مشاركته مع العالم؟</b>",
"app.components.GuidedTour.CTB.success.title": "الخطوة 1: ✅ مكتمل",
"app.components.GuidedTour.home.apiTokens.cta.title": "API اختبار ",
"app.components.GuidedTour.home.CM.title": "⚡️ ما الذي تود مشاركته مع العالم؟",
"app.components.GuidedTour.home.CTB.cta.title": "Content type Builder انتقل إلى",
"app.components.GuidedTour.home.CTB.title": "🧠 بناء هيكل المحتوى",
"app.components.GuidedTour.skip": "تخطي الجولة",
"app.components.GuidedTour.title": "خطوات للبدء ٣",
"app.components.HomePage.create": "قم بإنشاء نوع المحتوى الأول الخاص بك",
"app.components.HomePage.roadmap": "انظر خارطة الطريق لدينا",
"app.components.InstallPluginPage.Download.description": "قد يستغرق تنزيل المكون الإضافي وتثبيته بضع ثوانٍ",
"app.components.InstallPluginPage.Download.title": "جارى التحميل...",
"app.components.LeftMenu.collapse": "تصغير شريط التنقل",
"app.components.LeftMenu.expand": "قم بتوسيع شريط التنقل",
"app.components.LeftMenu.general": "عام",
"app.components.LeftMenu.logo.alt": "شعار التطبيق",
"app.components.LeftMenu.logout": "تسجيل خروج",
"app.components.LeftMenu.navbrand.title": "Strapi لوحة القيادة",
"app.components.LeftMenu.navbrand.workplace": "مكان العمل",
"app.components.LeftMenu.plugins": "الإضافات",
"app.components.LeftMenuFooter.help": "يساعد",
"app.components.LeftMenuLinkContainer.collectionTypes": "أنواع المجموعات",
"app.components.LeftMenuLinkContainer.singleTypes": "أنواع مفردة",
"app.components.ListPluginsPage.deletePlugin.description": "قد يستغرق الأمر بضع ثوان لإلغاء تثبيت المكون الإضافي",
"app.components.ListPluginsPage.deletePlugin.title": "إلغاء التثبيت",
"app.components.MarketplaceBanner": "اكتشف المكونات الإضافية التي أنشأها المجتمع ، والعديد من الأشياء الرائعة لبدء مشروعك ، في سوق Strapi.",
"app.components.MarketplaceBanner.image.alt": "شعار صاروخ Strapi",
"app.components.MarketplaceBanner.link": "افحصه الآن",
"app.components.Onboarding.help.button": "زر المساعدة",
"app.components.Onboarding.label.completed": "% مكتمل",
"app.components.Onboarding.link.build-content": "بناء بنية المحتوى",
"app.components.Onboarding.link.manage-content": "إضافة وإدارة المحتوى",
"app.components.Onboarding.link.manage-media": "إدارة الوسائط",
"app.components.Onboarding.link.more-videos": "شاهد المزيد من مقاطع الفيديو",
"app.components.Onboarding.title": "ابدأ مقاطع الفيديو",
"app.components.PluginCard.PopUpWarning.install.impossible.autoReload.needed": "يجب تمكين ميزة AutoReload. `yarn develop` يرجى بدء تطبيقك بـ .",
"app.components.PluginCard.PopUpWarning.install.impossible.confirm": "أفهم!",
"app.components.PluginCard.PopUpWarning.install.impossible.environment": "لأسباب أمنية ، لا يمكن تنزيل المكون الإضافي إلا في بيئة التطوير.",
"app.components.PluginCard.PopUpWarning.install.impossible.title": "التنزيل مستحيل",
"app.components.ToggleCheckbox.off-label": "خطأ",
"app.components.ToggleCheckbox.on-label": "حقيقي",
"app.components.Users.MagicLink.connect": "انسخ هذا الرابط وشاركه لمنح حق الوصول لهذا المستخدم",
"app.components.Users.MagicLink.connect.sso": "أرسل هذا الرابط إلى المستخدم ، حيث يمكن إجراء أول تسجيل دخول عبر موفر خدمة الدخول الموحد",
"app.components.Users.ModalCreateBody.block-title.details": "بيانات المستخدم",
"app.components.Users.ModalCreateBody.block-title.roles": "أدوار المستخدم",
"app.components.Users.ModalCreateBody.block-title.roles.description": "يمكن للمستخدم أن يكون له دور واحد أو عدة أدوار",
"app.components.Users.SortPicker.button-label": "ترتيب حسب",
"app.components.Users.SortPicker.sortby.email_asc": "البريد الإلكتروني (من الألف إلى الياء)",
"app.components.Users.SortPicker.sortby.email_desc": "بريد إلكتروني (من ي إلى أ)",
"app.components.Users.SortPicker.sortby.firstname_asc": "الاسم الأول (من الألف إلى الياء)",
"app.components.Users.SortPicker.sortby.firstname_desc": "الاسم الأول (ي إلى أ)",
"app.components.Users.SortPicker.sortby.lastname_asc": "الاسم الأخير (من الألف إلى الياء)",
"app.components.Users.SortPicker.sortby.lastname_desc": "الاسم الأخير (ي إلى أ)",
"app.components.Users.SortPicker.sortby.username_asc": "اسم المستخدم (من الألف إلى الياء)",
"app.components.Users.SortPicker.sortby.username_desc": "اسم المستخدم (من ي إلى أ)",
"app.containers.App.notification.error.init": "API حدث خطأ أثناء الطلب",
"app.containers.AuthPage.ForgotPasswordSuccess.text.contact-admin": "إذا لم تستلم هذا الرابط ، فيرجى الاتصال بالمسؤول.",
"app.containers.AuthPage.ForgotPasswordSuccess.text.email": "قد يستغرق استلام رابط استعادة كلمة المرور بضع دقائق.",
"app.containers.AuthPage.ForgotPasswordSuccess.title": "أرسل البريد الإلكتروني",
"app.containers.Users.EditPage.form.active.label": "فعال",
"app.containers.Users.EditPage.header.label": "تعديل {الاسم}",
"app.containers.Users.EditPage.header.label-loading": "تحرير العضو",
"app.containers.Users.EditPage.roles-bloc-title": "الأدوار المنسوبة",
"app.containers.Users.ModalForm.footer.button-success": "قم بدعوة المستخدم",
"app.links.configure-view": "تكوين العرض",
"app.page.not.found": "أُووبس! يبدو أنه لا يمكننا العثور على الصفحة التي تبحث عنها ...",
"app.static.links.cheatsheet": "ورقة الغش",
"app.utils.add-filter": "أضف عامل تصفية",
"app.utils.close-label": "يغلق",
"app.utils.delete": "يمسح",
"app.utils.duplicate": "ينسخ",
"app.utils.edit": "يحرر",
"app.utils.errors.file-too-big.message": "الملف كبير جدًا",
"app.utils.filter-value": "قيمة التصفية",
"app.utils.filters": "المرشحات",
"app.utils.notify.data-loaded": "{target} تم تحميل",
"app.utils.publish": "أصدر",
"app.utils.select-all": "اختر الكل",
"app.utils.select-field": "حدد المجال",
"app.utils.select-filter": "حدد عامل تصفية",
"app.utils.unpublish": "إلغاء النشر",
"Auth.components.Oops.text": "تم تعليق حسابك.",
"Auth.components.Oops.text.admin": "إذا كان هذا خطأ ، يرجى الاتصال بالمسؤول.",
"Auth.components.Oops.title": "أُووبس...",
"Auth.form.active.label": "فعال",
"Auth.form.button.go-home": "ارجع الى الصفحة الرئيسية",
"Auth.form.button.login.providers.error": "لا يمكننا توصيلك من خلال المزود المحدد.",
"Auth.form.button.login.strapi": "Strapi تسجيل الدخول عبر",
"Auth.form.button.password-recovery": "استعادة كلمة السر",
"Auth.form.confirmPassword.label": "تأكيد كلمة المرور",
"Auth.form.currentPassword.label": "كلمة السر الحالية",
"Auth.form.error.blocked": "تم حظر حسابك من قبل المسؤول.",
"Auth.form.error.confirmed": "لم يتم تأكيد البريد الإلكتروني لحسابك.",
"Auth.form.error.ratelimit": "محاولات كثيرة ، يرجى المحاولة مرة أخرى خلال دقيقة.",
"Auth.form.firstname.label": "الاسم الأول",
"Auth.form.firstname.placeholder": "على سبيل المثال سمر",
"Auth.form.lastname.label": "اسم العائلة",
"Auth.form.lastname.placeholder": "على سبيل المثال سامي",
"Auth.form.password.hide-password": "اخفاء كلمة المرور",
"Auth.form.password.hint": "يجب ألا يقل عدد الأحرف عن 8 أحرف ، وحرف كبير واحد ، ورقم واحد صغير ، ورقم واحد",
"Auth.form.password.show-password": "عرض كلمة المرور",
"Auth.form.register.news.label": "ابقني على اطلاع بالميزات الجديدة والتحسينات القادمة (من خلال القيام بذلك فأنت تقبل {terms} و ال {policy}).",
"Auth.form.register.subtitle": "تُستخدم بيانات الاعتماد فقط للمصادقة في Strapi. سيتم تخزين جميع البيانات المحفوظة في قاعدة البيانات الخاصة بك.",
"Auth.form.welcome.subtitle": "Strapi قم بتسجيل الدخول إلى حسابك على",
"Auth.form.welcome.title": "Strapi! مرحبا بك في",
"Auth.link.signin": "تسجيل الدخول",
"Auth.link.signin.account": "هل لديك حساب؟",
"Auth.login.sso.divider": "أو تسجيل الدخول باستخدام",
"Auth.login.sso.loading": "تحميل الموفرين ...",
"Auth.login.sso.subtitle": "SSO تسجيل الدخول إلى حسابك عبر",
"Auth.privacy-policy-agreement.policy": "سياسة الخصوصية",
"Auth.privacy-policy-agreement.terms": "شروط",
"Auth.reset-password.title": "إعادة تعيين كلمة المرور",
"clearLabel": "حذف",
"coming.soon": "هذا المحتوى قيد الإنشاء حاليًا وسيعود في غضون أسابيع قليلة!",
"component.Input.error.validation.integer": "يجب أن تكون القيمة عددًا صحيحًا",
"components.AutoReloadBlocker.description": "Strapi قم بتشغيل باستخدام أحد الأوامر التالية:",
"components.FilterOptions.FILTER_TYPES.$contains": "يحتوي على (حساس لحالة الأحرف)",
"components.FilterOptions.FILTER_TYPES.$endsWith": "ينتهي بـ",
"components.FilterOptions.FILTER_TYPES.$eq": "هو",
"components.FilterOptions.FILTER_TYPES.$gt": "أكبر من",
"components.FilterOptions.FILTER_TYPES.$gte": "أكبر من أو يساوي",
"components.FilterOptions.FILTER_TYPES.$lt": "أقل من",
"components.FilterOptions.FILTER_TYPES.$lte": "أقل من أو يساوي",
"components.FilterOptions.FILTER_TYPES.$ne": "ليس",
"components.FilterOptions.FILTER_TYPES.$notContains": "لا يحتوي على (حساس لحالة الأحرف)",
"components.FilterOptions.FILTER_TYPES.$notNull": "هو ليس لاشيء",
"components.FilterOptions.FILTER_TYPES.$null": "هو لاشيء",
"components.FilterOptions.FILTER_TYPES.$startsWith": "يبدا ب",
"components.Input.error.contain.lowercase": "يجب أن تحتوي كلمة المرور على حرف صغير واحد على الأقل",
"components.Input.error.contain.number": "يجب ان تحتوي كلمة المرور على الاقل رقما واحدا",
"components.Input.error.contain.uppercase": "يجب أن تحتوي كلمة المرور على حرف كبير واحد على الأقل",
"components.Input.error.validation.lowercase": "يجب أن تكون القيمة سلسلة أحرف صغيرة",
"components.Input.error.validation.unique": "هذه القيمة مستخدمة بالفعل.",
"components.InputSelect.option.placeholder": "اختر هنا",
"components.NotAllowedInput.text": "لا أذونات لرؤية هذا المجال",
"components.OverlayBlocker.description.serverError": "يجب إعادة تشغيل الخادم ، يرجى التحقق من سجلاتك في المحطة.",
"components.OverlayBlocker.title.serverError": "تستغرق إعادة التشغيل وقتًا أطول من المتوقع",
"components.pagination.go-to": "{page} انتقل إلى صفحة",
"components.pagination.go-to-next": "انتقل إلى الصفحة التالية",
"components.pagination.go-to-previous": "الانتقال إلى الصفحة السابقة",
"components.pagination.remaining-links": "روابط أخرى{number} و",
"components.popUpWarning.button.cancel": "لا ، إلغاء",
"components.popUpWarning.button.confirm": "نعم ، قم بالتأكيد",
"components.Search.placeholder": "بحث...",
"components.TableHeader.sort": "{label} الفرز على",
"components.Wysiwyg.ToggleMode.markdown-mode": "وضع Markdown",
"components.Wysiwyg.ToggleMode.preview-mode": "وضعية المعاينة",
"Content Type Builder": "منشئ أنواع المحتوى",
"content-manager.api.id": "معرف API",
"content-manager.apiError.This attribute must be unique": "{field} يجب أن يكون فريدًا",
"content-manager.App.schemas.data-loaded": "تم تحميل المخططات بنجاح",
"content-manager.components.DraggableCard.delete.field": "{item} حذف",
"content-manager.components.DraggableCard.edit.field": "{item} حرر",
"content-manager.components.DraggableCard.move.field": "{item} تحرك",
"content-manager.components.DragHandle-label": "جر",
"content-manager.components.DynamicTable.row-line": "{number} سطر البند",
"content-manager.components.DynamicZone.add-component": "{componentName} أضف مكونًا إلى",
"content-manager.components.DynamicZone.ComponentPicker-label": "اختر مكونًا واحدًا",
"content-manager.components.DynamicZone.delete-label": "{name} حذف",
"content-manager.components.DynamicZone.error-message": "يحتوي المكون على خطأ (أخطاء)",
"content-manager.components.DynamicZone.missing-components": "There {number, plural, =0 {are # missing components} واحد {is # missing component} آخر {are # missing components}}",
"content-manager.components.DynamicZone.move-down-label": "انقل المكون لأسفل",
"content-manager.components.DynamicZone.move-up-label": "انقل المكون لأعلى",
"content-manager.components.DynamicZone.pick-compo": "اختر مكونًا واحدًا",
"content-manager.components.DynamicZone.required": "المكون مطلوب",
"content-manager.components.empty-repeatable": "لا دخول حتى الان. انقر فوق الزر أدناه لإضافة واحد.",
"content-manager.components.FieldItem.linkToComponentLayout": "قم بتعيين تخطيط المكون",
"content-manager.components.FieldSelect.label": "أضف حقلاً",
"content-manager.components.LeftMenu.collection-types": "أنواع المجموعات",
"content-manager.components.LeftMenu.Search.label": "ابحث عن نوع المحتوى",
"content-manager.components.LeftMenu.single-types": "أنواع مفردة",
"content-manager.components.NotAllowedInput.text": "لا أذونات لرؤية هذا المجال",
"content-manager.components.notification.info.maximum-requirement": "لقد وصلت بالفعل إلى الحد الأقصى لعدد الحقول",
"content-manager.components.notification.info.minimum-requirement": "تمت إضافة حقل لمطابقة الحد الأدنى من المتطلبات",
"content-manager.components.RelationInput.icon-button-aria-label": "جر",
"content-manager.components.repeatable.reorder.error": "حدث خطأ أثناء إعادة ترتيب حقل المكون الخاص بك ، يرجى المحاولة مرة أخرى",
"content-manager.components.RepeatableComponent.error-message": "يحتوي المكون (المكونات) على خطأ (أخطاء)",
"content-manager.components.reset-entry": "إعادة الدخول",
"content-manager.components.Select.draft-info-title": "الحالة: مسودة",
"content-manager.components.Select.publish-info-title": "الحالة: منشور",
"content-manager.components.SettingsViewWrapper.pluginHeader.description.edit-settings": "تخصيص كيف سيبدو عرض التحرير.",
"content-manager.components.SettingsViewWrapper.pluginHeader.description.list-settings": "حدد إعدادات عرض القائمة.",
"content-manager.components.SettingsViewWrapper.pluginHeader.title": "{name} تكوين العرض -",
"content-manager.components.TableDelete.label": "{number, plural, one {# entry} آخر {# entries}} selected",
"content-manager.components.uid.apply": "طبق",
"content-manager.components.uid.available": "متاح",
"content-manager.components.uid.regenerate": "تجديد",
"content-manager.components.uid.suggested": "مقترح",
"content-manager.components.uid.unavailable": "غير متوفره",
"content-manager.containers.Edit.delete-entry": "احذف هذا الإدخال",
"content-manager.containers.Edit.information": "معلومة",
"content-manager.containers.Edit.information.by": "بواسطة",
"content-manager.containers.Edit.information.created": "أُنشء",
"content-manager.containers.Edit.information.draftVersion": "نسخة المسودة",
"content-manager.containers.Edit.information.editing": "التحرير",
"content-manager.containers.Edit.information.lastUpdate": "اخر تحديث",
"content-manager.containers.Edit.information.publishedVersion": "النسخة المنشورة",
"content-manager.containers.Edit.Link.Layout": "تكوين التخطيط",
"content-manager.containers.Edit.Link.Model": "تحرير نوع المجموعة",
"content-manager.containers.Edit.pluginHeader.title.new": "قم بإنشاء إدخال",
"content-manager.containers.EditSettingsView.modal-form.edit-field": "قم بتحرير الحقل",
"content-manager.containers.EditView.add.new-entry": "أضف إدخالاً",
"content-manager.containers.EditView.notification.errors": "النموذج يحتوي على بعض الأخطاء",
"content-manager.containers.List.draft": "مسودة",
"content-manager.containers.List.published": "نشرت",
"content-manager.containers.ListPage.items": "{number, plural, =0 {items} one {item} other {items}}",
"content-manager.containers.ListPage.table-headers.publishedAt": "State",
"content-manager.containers.ListSettingsView.modal-form.edit-label": "{fieldName} تعديل",
"content-manager.containers.SettingPage.add.field": "أدخل حقل آخر",
"content-manager.containers.SettingPage.add.relational-field": "أدخل حقل آخر ذي صلة",
"content-manager.containers.SettingPage.editSettings.entry.title": "عنوان الإدخال",
"content-manager.containers.SettingPage.editSettings.entry.title.description": "اضبط الحقل المعروض لإدخالك",
"content-manager.containers.SettingPage.editSettings.relation-field.description": "قم بتعيين الحقل المعروض في كل من طريقتي التحرير وعرض القائمة",
"content-manager.containers.SettingPage.layout": "تَخطِيط",
"content-manager.containers.SettingPage.listSettings.description": "تكوين الخيارات لنوع المجموعة هذا",
"content-manager.containers.SettingPage.pluginHeaderDescription": "تكوين الإعدادات المحددة لنوع المجموعة هذا",
"content-manager.containers.SettingPage.relations": "حقول ذات صله",
"content-manager.containers.SettingPage.settings": "إعدادات",
"content-manager.containers.SettingPage.view": "رؤية",
"content-manager.containers.SettingsPage.Block.contentType.title": "أنواع المجموعات",
"content-manager.containers.SettingsPage.Block.generalSettings.description": "تكوين الخيارات الافتراضية لأنواع المجموعة الخاصة بك",
"content-manager.containers.SettingsPage.pluginHeaderDescription": "قم بتكوين الإعدادات لجميع أنواع المجموعات والمجموعات الخاصة بك",
"content-manager.containers.SettingsView.list.subtitle": "تكوين تخطيط وعرض أنواع المجموعات والمجموعات الخاصة بك",
"content-manager.containers.SettingsView.list.title": "تكوينات العرض",
"content-manager.containers.SettingViewModel.pluginHeader.title": "{name} مدير محتوى -",
"content-manager.dnd.cancel-item": "{item}, dropped. Re-order cancelled.",
"content-manager.dnd.drop-item": "{item}, dropped. Final position in list: {position}.",
"content-manager.dnd.grab-item": "{item}, grabbed. Current position in list: {position}. Press up and down arrow to change position, Spacebar to drop, Escape to cancel.",
"content-manager.dnd.instructions": "اضغط على مفتاح المسافة للاستيلاء وإعادة الترتيب",
"content-manager.dnd.reorder": "{item}, انتقل. منصب جديد في القائمة: {position}.",
"content-manager.DynamicTable.relation-loaded": "تم تحميل العلاقات",
"content-manager.DynamicTable.relation-loading": "يتم تحميل العلاقات",
"content-manager.DynamicTable.relation-more": "تحتوي هذه العلاقة على كيانات أكثر من المعروضة",
"content-manager.edit-settings-view.link-to-ctb.components": "قم بتحرير المكون",
"content-manager.edit-settings-view.link-to-ctb.content-types": "قم بتحرير نوع المحتوى",
"content-manager.emptyAttributes.button": "انتقل إلى منشئ نوع المجموعة",
"content-manager.emptyAttributes.description": "أضف حقلك الأول إلى نوع المجموعة الخاصة بك",
"content-manager.form.Input.hint.character.unit": "{maxValue, plural, one { character} other { characters}}",
"content-manager.form.Input.hint.minMaxDivider": " / ",
"content-manager.form.Input.hint.text": "{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}",
"content-manager.form.Input.pageEntries.inputDescription": "ملاحظة: يمكنك تجاوز هذه القيمة في صفحة إعدادات نوع المجموعة.",
"content-manager.form.Input.sort.order": "ترتيب الافتراضي",
"content-manager.form.Input.wysiwyg": "WYSIWYG عرض كـ",
"content-manager.global.displayedFields": "الحقول المعروضة",
"content-manager.groups": "مجموعات",
"content-manager.groups.numbered": "({number}) مجموعات",
"content-manager.header.name": "محتوى",
"content-manager.HeaderLayout.button.label-add-entry": "إنشاء إدخال جديد",
"content-manager.link-to-ctb": "قم بتحرير النموذج",
"content-manager.models": "أنواع المجموعات",
"content-manager.models.numbered": "({number}) أنواع المجموعات",
"content-manager.notification.info.minimumFields": "يجب أن يكون لديك حقل واحد على الأقل معروض",
"content-manager.notification.upload.error": "حدث خطأ أثناء تحميل ملفاتك",
"content-manager.pages.ListView.header-subtitle": "{number, plural, =0 {# entries} one {# entry} other {# entries}} found",
"content-manager.pages.NoContentType.button": "قم بإنشاء نوع المحتوى الأول الخاص بك",
"content-manager.pages.NoContentType.text": "ليس لديك أي محتوى حتى الآن ، نوصيك بإنشاء نوع المحتوى الأول الخاص بك.",
"content-manager.permissions.not-allowed.create": "لا يسمح لك لإنشاء وثيقة",
"content-manager.permissions.not-allowed.update": "لا يسمح لك أن ترى هذه الوثيقة",
"content-manager.popover.display-relations.label": "عرض العلاقات",
"content-manager.popUpwarning.warning.has-draft-relations.button-confirm": "نعم ، انشر",
"content-manager.popUpwarning.warning.has-draft-relations.message": "<b>{count, plural, one { relation is } آخر { relations are } }</b> لم تنشر بعد وقد تؤدي إلى سلوك غير متوقع.",
"content-manager.popUpWarning.warning.has-draft-relations.title": "تأكيد",
"content-manager.popUpWarning.warning.publish-question": "هل مازلت تريد النشر؟",
"content-manager.popUpWarning.warning.unpublish": "إذا لم تنشر هذا المحتوى ، فسيتحول تلقائيًا إلى مسودة.",
"content-manager.popUpWarning.warning.unpublish-question": "هل أنت متأكد أنك لا تريد نشره؟",
"content-manager.relation.add": "أضف العلاقة",
"content-manager.relation.disconnect": "نزع",
"content-manager.relation.isLoading": "يتم تحميل العلاقات",
"content-manager.relation.loadMore": "تحميل المزيد",
"content-manager.relation.notAvailable": "لا توجد علاقات متاحة",
"content-manager.relation.publicationState.draft": "مسودة",
"content-manager.relation.publicationState.published": "منشور",
"content-manager.select.currently.selected": "{count} المحدد حاليا",
"content-manager.success.record.publish": "منشور",
"content-manager.success.record.unpublish": "غير منشورة",
"content-manager.utils.data-loaded": "The {number, plural, =1 {entry has} other {entries have}} successfully been loaded",
"dark": "داكن",
"Documentation": "توثيق",
"form.button.continue": "واصل",
"form.button.done": "منتهي",
"global.actions": "أجراءات",
"global.auditLogs": "سجلات التدقيق",
"global.back": "الى الوراء",
"global.cancel": "إلغاء",
"global.change-password": "تغيير كلمة المرور",
"global.content-manager": "مدير محتوى",
"global.continue": "واصل",
"global.delete": "مسح",
"global.delete-target": "{target} مسح ",
"global.description": "وصف",
"global.details": "تفاصيل",
"global.disabled": "إبطال",
"global.documentation": "توثيق",
"global.enabled": "ممكن",
"global.finish": "نهاية",
"global.marketplace": "المتجر",
"global.name": "اسم",
"global.none": "لا أحد",
"global.password": "كلمة المرور",
"global.plugins": "الإضافات",
"global.plugins.content-manager": "مدير محتوى",
"global.plugins.content-manager.description": "طريقة سريعة لرؤية وتحرير وحذف البيانات في قاعدة البيانات الخاصة بك.",
"global.plugins.content-type-builder": "منشئ نوع المحتوى",
"global.plugins.content-type-builder.description": "قم بنمذجة بنية البيانات الخاصة بواجهة برمجة التطبيقات (API) الخاصة بك. إنشاء مجالات وعلاقات جديدة في دقيقة واحدة فقط. يتم إنشاء الملفات وتحديثها تلقائيًا في مشروعك.",
"global.plugins.documentation": "توثيق",
"global.plugins.documentation.description": "قم بإنشاء مستند OpenAPI وتصور API الخاص بك باستخدام SWAGGER UI.",
"global.plugins.email": "بريد إلكتروني",
"global.plugins.email.description": "تكوين التطبيق الخاص بك لإرسال رسائل البريد الإلكتروني.",
"global.plugins.graphql": "GraphQL",
"global.plugins.graphql.description": "يضيف نقطة نهاية GraphQL بأساليب واجهة برمجة التطبيقات الافتراضية.",
"global.plugins.i18n": "تدويل",
"global.plugins.i18n.description": "يمكّن هذا المكون الإضافي من إنشاء المحتوى وقراءته وتحديثه بلغات مختلفة ، سواء من لوحة الإدارة أو من واجهة برمجة التطبيقات.",
"global.plugins.sentry": "Sentry",
"global.plugins.sentry.description": "إرسال أحداث خطأ Strapi إلى Sentry.",
"global.plugins.upload": "مكتبة الوسائط",
"global.plugins.upload.description": "إدارة ملفات الوسائط.",
"global.plugins.users-permissions": "الأدوار والأذونات",
"global.plugins.users-permissions.description": "قم بحماية API الخاص بك من خلال عملية مصادقة كاملة تعتمد على JWT. يأتي هذا المكون الإضافي أيضًا مع إستراتيجية قائمة التحكم بالوصول (ACL) التي تتيح لك إدارة الأذونات بين مجموعات المستخدمين.",
"global.profile": "حساب تعريفي",
"global.prompt.unsaved": "هل أنت متأكد أنك تريد مغادرة هذه الصفحة؟ ستفقد كل تعديلاتك",
"global.reset-password": "إعادة تعيين كلمة المرور",
"global.roles": "الأدوار",
"global.save": "يحفظ",
"global.search": "يبحث",
"global.see-more": "شاهد المزيد",
"global.select": "اختار",
"global.select-all-entries": "حدد كل الإدخالات",
"global.settings": "إعدادات",
"global.type": "نوع",
"global.users": "المستخدمون",
"HomePage.helmet.title": "الصفحة الرئيسية",
"HomePage.roadmap": "انظر خارطة الطريق لدينا",
"HomePage.welcome.congrats": "تهاني!",
"HomePage.welcome.congrats.content": "Strapiلقد قمت بتسجيل الدخول باعتبارك المسؤول الأول. لاكتشاف الميزات القوية التي يوفرها",
"HomePage.welcome.congrats.content.bold": "نوصيك بإنشاء أول نوع مجموعة خاص بك.",
"light": "فاتح",
"Media Library": "مكتبة الوسائط",
"notification.contentType.relations.conflict": "نوع المحتوى له علاقات متضاربة",
"notification.default.title": "معلومة:",
"notification.ee.warning.at-seat-limit.title": "{LicenseLimitStatus ، حدد ، OVER_LIMIT {Over} AT_LIMIT {At}} حد المقاعد ({currentUserCount} / {allowedSeats})",
"notification.ee.warning.over-.message": "أضف مقاعد إلى {LicenseLimitStatus ، حدد ، OVER_LIMIT {دعوة} AT_LIMIT {re-enable}} مستخدمين. إذا كنت قد فعلت ذلك بالفعل ولكن لم ينعكس في Strapi بعد ، فتأكد من إعادة تشغيل التطبيق الخاص بك.",
"notification.error.invalid.configuration": "لديك تكوين غير صالح ، تحقق من سجل الخادم لمزيد من المعلومات.",
"notification.error.tokennamenotunique": "تم تعيين الاسم بالفعل لرمز مميز آخر",
"notification.form.error.fields": "النموذج يحتوي على بعض الأخطاء",
"notification.form.success.fields": "تم حفظ التغييرات",
"notification.link-copied": "تم نسخ الرابط في الحافظة",
"notification.permission.not-allowed-read": "لا يسمح لك أن ترى هذه الوثيقة",
"notification.success.apitokencreated": "تم إنشاء رمز API بنجاح",
"notification.success.apitokenedited": "تم تحرير رمز API بنجاح",
"notification.success.delete": "تم حذف العنصر",
"notification.success.saved": "حفظ",
"notification.success.title": "نجاح:",
"notification.success.transfertokencreated": "تم إنشاء رمز النقل بنجاح",
"notification.success.transfertokenedited": "تم تحرير رمز النقل بنجاح",
"notification.version.update.message": "نسخة جديدة متاحة من ستربي!",
"notification.warning.404": "404 غير موجود",
"notification.warning.title": "تحذير:",
"or": "أو",
"Roles & Permissions": "الأدوار والأذونات",
"Roles.components.List.empty.withSearch": "لا يوجد دور مطابق للبحث ({search}) ...",
"Roles.ListPage.notification.delete-all-not-allowed": "تعذر حذف بعض الأدوار لأنها مرتبطة بالمستخدمين",
"Roles.ListPage.notification.delete-not-allowed": "لا يمكن حذف الدور إذا كان مرتبطًا بالمستخدمين",
"Roles.RoleRow.select-all": "حدد {name} للإجراءات المجمعة",
"Roles.RoleRow.user-count": "{number، plural، = 0 {# user} واحد {# user} آخر {# users}}",
"selectButtonTitle": "يختار",
"Settings.apiTokens.addFirstToken": "أضف رمز API الأول الخاص بك",
"Settings.apiTokens.addNewToken": "إضافة رمز API جديد",
"Settings.apiTokens.create": "إنشاء رمز API جديد",
"Settings.apiTokens.createPage.BoundRoute.title": "طريق منضم إلى",
"Settings.apiTokens.createPage.permissions.description": "يتم سرد الإجراءات المرتبطة بالمسار فقط أدناه.",
"Settings.apiTokens.createPage.permissions.header.hint": "حدد إجراءات التطبيق أو إجراءات البرنامج المساعد وانقر على أيقونة الترس لعرض المسار المنضم",
"Settings.apiTokens.createPage.permissions.header.title": "إعدادات متقدمة",
"Settings.apiTokens.createPage.permissions.title": "أذونات",
"Settings.apiTokens.createPage.title": "إنشاء رمز API",
"Settings.apiTokens.description": "قائمة الرموز التي تم إنشاؤها لاستهلاك API",
"Settings.apiTokens.emptyStateLayout": "ليس لديك أي محتوى حتى الآن ...",
"Settings.apiTokens.ListView.headers.createdAt": "أنشئت في",
"Settings.apiTokens.ListView.headers.description": "وصف",
"Settings.apiTokens.ListView.headers.lastUsedAt": "آخر أستخدام",
"Settings.apiTokens.ListView.headers.name": "اسم",
"Settings.apiTokens.ListView.headers.type": "نوع الرمز",
"Settings.apiTokens.regenerate": "تجديد",
"Settings.apiTokens.title": "رموز API",
"Settings.application.customization": "التخصيص",
"Settings.application.customization.auth-logo.carousel-hint": "استبدل الشعار في صفحات المصادقة",
"Settings.application.customization.carousel-hint": "تغيير شعار لوحة الإدارة (الحد الأقصى للبعد: {dimension} {dimension} ، الحد الأقصى لحجم الملف: {size} كيلوبايت)",
"Settings.application.customization.carousel-slide.label": "شريحة الشعار",
"Settings.application.customization.carousel.auth-logo.title": "شعار Auth",
"Settings.application.customization.carousel.change-action": "تغيير الشعار",
"Settings.application.customization.carousel.menu-logo.title": "شعار القائمة",
"Settings.application.customization.carousel.reset-action": "إعادة تعيين الشعار",
"Settings.application.customization.carousel.title": "شعار",
"Settings.application.customization.menu-logo.carousel-hint": "استبدل الشعار في شريط التنقل الرئيسي",
"Settings.application.customization.modal.cancel": "إلغاء",
"Settings.application.customization.modal.pending": "شعار معلق",
"Settings.application.customization.modal.pending.card-badge": "صورة",
"Settings.application.customization.modal.pending.choose-another": "اختر شعارًا آخر",
"Settings.application.customization.modal.pending.subtitle": "إدارة الشعار المختار قبل تحميله",
"Settings.application.customization.modal.pending.title": "الشعار جاهز للتحميل",
"Settings.application.customization.modal.pending.upload": "تحميل الشعار",
"Settings.application.customization.modal.tab.label": "كيف تريد تحميل الأصول الخاصة بك؟",
"Settings.application.customization.modal.upload": "تحميل الشعار",
"Settings.application.customization.modal.upload.cta.browse": "تصفح ملفات",
"Settings.application.customization.modal.upload.drag-drop": "قم بالسحب والإفلات هنا أو",
"Settings.application.customization.modal.upload.error-format": "تم تحميل تنسيق خاطئ (التنسيقات المقبولة فقط: jpeg ، jpg ، png ، svg).",
"Settings.application.customization.modal.upload.error-network": "خطأ في الشبكة",
"Settings.application.customization.modal.upload.error-size": "الملف الذي تم تحميله كبير جدًا (الحد الأقصى للبعد: {dimension} x {dimension} ، الحد الأقصى لحجم الملف: {size} كيلوبايت)",
"Settings.application.customization.modal.upload.file-validation": "أقصى بُعد: {dimension} x {dimension} ، الحد الأقصى للحجم: {size} كيلوبايت",
"Settings.application.customization.modal.upload.from-computer": "من الكمبيوتر",
"Settings.application.customization.modal.upload.from-url": "من URL",
"Settings.application.customization.modal.upload.from-url.input-label": "URL",
"Settings.application.customization.modal.upload.next": "التالي",
"Settings.application.customization.size-details": "أقصى بُعد: {dimension} x {dimension} ، الحد الأقصى لحجم الملف: {size} كيلوبايت",
"Settings.application.description": "المعلومات العالمية للوحة الإدارة",
"Settings.application.edition-title": "الخطة الحالية",
"Settings.application.ee-or-ce": "{communityEdition، select، true {Community Edition} أخرى {Enterprise Edition}}",
"Settings.application.ee.admin-seats.add-seats": "{isHostedOnStrapiCloud، select، true {Add seat} other {Contact sales}}",
"Settings.application.ee.admin-seats.at-limit-tooltip": "عند الحد: أضف مقاعد لدعوة المزيد من المستخدمين",
"Settings.application.ee.admin-seats.count": "<text>{enforcementUserCount}</text>/{permittedSeats}",
"Settings.application.get-help": "احصل على مساعدة",
"Settings.application.link-pricing": "انظر جميع خطط التسعير",
"Settings.application.link-upgrade": "قم بترقية لوحة الإدارة الخاصة بك",
"Settings.application.node-version": "إصدار العقدة",
"Settings.application.strapi-version": "نسخة ستربي",
"Settings.application.strapiVersion": "Strapi نسخة",
"Settings.application.title": "ملخص",
"Settings.error": "خطأ",
"Settings.global": "الاعدادات العامة",
"Settings.PageTitle": "الإعدادات - {name}",
"Settings.permissions": "لوحة الإدارة",
"Settings.permissions.auditLogs.action": "فعل",
"Settings.permissions.auditLogs.admin.auth.success": "دخول المشرف",
"Settings.permissions.auditLogs.admin.logout": "خروج المسؤول",
"Settings.permissions.auditLogs.component.create": "تكوين المكون",
"Settings.permissions.auditLogs.component.delete": "حذف المكون",
"Settings.permissions.auditLogs.component.update": "مكون التحديث",
"Settings.permissions.auditLogs.content-type.create": "إنشاء نوع المحتوى",
"Settings.permissions.auditLogs.content-type.delete": "حذف نوع المحتوى",
"Settings.permissions.auditLogs.content-type.update": "تحديث نوع المحتوى",
"Settings.permissions.auditLogs.date": "تاريخ",
"Settings.permissions.auditLogs.details": "تفاصيل السجل",
"Settings.permissions.auditLogs.entry.create": "إنشاء إدخال {model، select، undefined {} other {({model})}}",
"Settings.permissions.auditLogs.entry.delete": "حذف الإدخال {model، select، undefined {} other {({model})}}",
"Settings.permissions.auditLogs.entry.publish": "نشر الإدخال {model، select، undefined {} other {({model})}}",
"Settings.permissions.auditLogs.entry.unpublish": "إلغاء نشر الإدخال {model، select، undefined {} other {({model})}}",
"Settings.permissions.auditLogs.entry.update": "تحديث الإدخال {model، select، undefined {} other {({model})}}",
"Settings.permissions.auditLogs.filters.combobox.aria-label": "ابحث وحدد خيارًا للتصفية",
"Settings.permissions.auditLogs.listview.header.subtitle": "سجلات لجميع الأنشطة التي حدثت في بيئتك",
"Settings.permissions.auditLogs.media.create": "قم بإنشاء وسائط",
"Settings.permissions.auditLogs.media.delete": "حذف الوسائط",
"Settings.permissions.auditLogs.media.update": "تحديث الوسائط",
"Settings.permissions.auditLogs.payload": "الحمولة",
"Settings.permissions.auditLogs.permission.create": "إنشاء إذن",
"Settings.permissions.auditLogs.permission.delete": "حذف إذن",
"Settings.permissions.auditLogs.permission.update": "إذن التحديث",
"Settings.permissions.auditLogs.role.create": "خلق دور",
"Settings.permissions.auditLogs.role.delete": "حذف الدور",
"Settings.permissions.auditLogs.role.update": "تحديث الدور",
"Settings.permissions.auditLogs.user": "مستخدم",
"Settings.permissions.auditLogs.user.create": "إنشاء مستخدم",
"Settings.permissions.auditLogs.user.delete": "مسح المستخدم",
"Settings.permissions.auditLogs.user.fullname": "{firstname} {lastname}",
"Settings.permissions.auditLogs.user.update": "تحديث المستخدم",
"Settings.permissions.auditLogs.userId": "معرف المستخدم",
"Settings.permissions.category": "إعدادات الأذونات لـ {category}",
"Settings.permissions.category.plugins": "إعدادات الأذونات للمكوِّن الإضافي {category}",
"Settings.permissions.conditions.anytime": "في أي وقت",
"Settings.permissions.conditions.apply": "يتقدم",
"Settings.permissions.conditions.can": "يستطيع",
"Settings.permissions.conditions.conditions": "شروط",
"Settings.permissions.conditions.define-conditions": "حدد الشروط",
"Settings.permissions.conditions.links": "الروابط",
"Settings.permissions.conditions.no-actions": "تحتاج أولاً إلى تحديد الإجراءات (إنشاء ، قراءة ، تحديث ، ...) قبل تحديد الشروط عليها.",
"Settings.permissions.conditions.none-selected": "في أي وقت",
"Settings.permissions.conditions.or": "أو",
"Settings.permissions.conditions.when": "متى",
"Settings.permissions.select-all-by-permission": "حدد كافة أذونات {label}",
"Settings.permissions.select-by-permission": "اختار {label} إذن",
"Settings.permissions.users.active": "نشيط",
"Settings.permissions.users.create": "قم بدعوة مستخدم جديد",
"Settings.permissions.users.email": "بريد إلكتروني",
"Settings.permissions.users.firstname": "الاسم الأول",
"Settings.permissions.users.form.sso": "تواصل مع SSO",
"Settings.permissions.users.form.sso.description": "عند التمكين (ON) ، يمكن للمستخدمين تسجيل الدخول عبر SSO",
"Settings.permissions.users.inactive": "غير نشط",
"Settings.permissions.users.lastname": "اسم العائلة",
"Settings.permissions.users.listview.header.subtitle": "جميع المستخدمين الذين لديهم حق الوصول إلى لوحة إدارة Strapi",
"Settings.permissions.users.roles": "الأدوار",
"Settings.permissions.users.strapi-author": "مؤلف",
"Settings.permissions.users.strapi-editor": "محرر",
"Settings.permissions.users.strapi-super-admin": "مشرف فائق",
"Settings.permissions.users.tabs.label": "أذونات علامات التبويب",
"Settings.permissions.users.user-status": "حالة المستخدم",
"Settings.permissions.users.username": "اسم المستخدم",
"Settings.profile.form.notify.data.loaded": "تم تحميل بيانات ملفك الشخصي",
"Settings.profile.form.section.experience.clear.select": "امسح لغة الواجهة المحددة",
"Settings.profile.form.section.experience.here": "هنا",
"Settings.profile.form.section.experience.interfaceLanguage": "لغة الواجهة",
"Settings.profile.form.section.experience.interfaceLanguage.hint": "سيعرض هذا فقط واجهتك الخاصة باللغة المختارة.",
"Settings.profile.form.section.experience.interfaceLanguageHelp": "سيتم تطبيق تغييرات التفضيلات عليك فقط. يتوفر مزيد من المعلومات{here}.",
"Settings.profile.form.section.experience.mode.hint": "يعرض واجهتك في الوضع المختار.",
"Settings.profile.form.section.experience.mode.label": "وضع الواجهة",
"Settings.profile.form.section.experience.mode.option-label": "{name} وضع",
"Settings.profile.form.section.experience.title": "خبرة",
"Settings.profile.form.section.helmet.title": "ملف تعريفي للمستخدم",
"Settings.profile.form.section.profile.page.title": "الصفحة الشخصية",
"Settings.roles.create.description": "تحديد الحقوق الممنوحة للدور",
"Settings.roles.create.title": "أنشئ دورًا",
"Settings.roles.created": "تم إنشاء الدور",
"Settings.roles.edit.title": "تحرير دور",
"Settings.roles.form.button.users-with-role": "{number, plural, =0 {# users} one {# user} other {# users}} with this role",
"Settings.roles.form.created": "مكون",
"Settings.roles.form.description": "اسم ووصف الدور",
"Settings.roles.form.permission.property-label": "{label} أذونات",
"Settings.roles.form.permissions.attributesPermissions": "أذونات الحقول",
"Settings.roles.form.permissions.create": "خلق",
"Settings.roles.form.permissions.delete": "شطب",
"Settings.roles.form.permissions.publish": "ينشر",
"Settings.roles.form.permissions.read": "يقرأ",
"Settings.roles.form.permissions.update": "تحديث",
"Settings.roles.list.button.add": "أضف دورًا جديدًا",
"Settings.roles.list.description": "قائمة الأدوار",
"Settings.roles.title.singular": "دور",
"Settings.sso.description": "قم بتكوين الإعدادات لميزة الدخول الموحد.",
"Settings.sso.form.defaultRole.description": "سيقوم بإرفاق المستخدم الجديد المصادق عليه بالدور المحدد",
"Settings.sso.form.defaultRole.description-not-allowed": "تحتاج إلى الحصول على إذن لقراءة أدوار المسؤول",
"Settings.sso.form.defaultRole.label": "الدور الافتراضي",
"Settings.sso.form.registration.description": "أنشئ مستخدمًا جديدًا على تسجيل الدخول الموحّد (SSO) في حالة عدم وجود حساب",
"Settings.sso.form.registration.label": "التسجيل التلقائي",
"Settings.sso.title": "علامة واحدة على",
"Settings.tokens.Button.cancel": "يلغي",
"Settings.tokens.Button.regenerate": "تجديد",
"Settings.tokens.copy.editMessage": "لأسباب تتعلق بالأمان ، لا يمكنك رؤية رمزك المميز إلا مرة واحدة.",
"Settings.tokens.copy.editTitle": "لم يعد هذا الرمز المميز يمكن الوصول إليه.",
"Settings.tokens.copy.lastWarning": "تأكد من نسخ هذا الرمز المميز ، فلن تتمكن من رؤيته مرة أخرى!",
"Settings.tokens.duration.30-days": "30 يوما",
"Settings.tokens.duration.7-days": "7 أيام",
"Settings.tokens.duration.90-days": "90 يومًا",
"Settings.tokens.duration.expiration-date": "تاريخ انتهاء الصلاحية",
"Settings.tokens.duration.unlimited": "غير محدود",
"Settings.tokens.form.description": "وصف",
"Settings.tokens.form.duration": "مدة الرمز",
"Settings.tokens.form.name": "اسم",
"Settings.tokens.form.type": "نوع الرمز",
"Settings.tokens.ListView.headers.createdAt": "أنشئت في",
"Settings.tokens.ListView.headers.description": "وصف",
"Settings.tokens.ListView.headers.lastUsedAt": "آخر أستخدام",
"Settings.tokens.ListView.headers.name": "اسم",
"Settings.tokens.notification.copied": "تم نسخ الرمز المميز إلى الحافظة.",
"Settings.tokens.popUpWarning.message": "هل أنت متأكد أنك تريد إعادة إنشاء هذا الرمز المميز؟",
"Settings.tokens.regenerate": "تجديد",
"Settings.tokens.RegenerateDialog.title": "إعادة إنشاء الرمز المميز",
"Settings.tokens.types.custom": "مخصص",
"Settings.tokens.types.full-access": "الوصول الكامل",
"Settings.tokens.types.read-only": "يقرأ فقط",
"Settings.transferTokens.addFirstToken": "أضف أول رمز تحويل خاص بك",
"Settings.transferTokens.addNewToken": "أضف رمز تحويل جديد",
"Settings.transferTokens.create": "إنشاء رمز تحويل جديد",
"Settings.transferTokens.createPage.title": "إنشاء رمز التحويل",
"Settings.transferTokens.description": "قائمة برموز التحويل المُنشأة",
"Settings.transferTokens.emptyStateLayout": "ليس لديك أي محتوى حتى الآن ...",
"Settings.transferTokens.ListView.headers.type": "نوع الرمز",
"Settings.transferTokens.title": "رموز التحويل",
"Settings.webhooks.create": "إنشاء خطاف ويب",
"Settings.webhooks.create.header": "إنشاء رأس جديد",
"Settings.webhooks.created": "تم إنشاء الرد التلقائي على الويب",
"Settings.webhooks.event.publish-tooltip": "هذا الحدث موجود فقط للمحتويات مع تمكين نظام المسودة / النشر",
"Settings.webhooks.events.create": "أخلق",
"Settings.webhooks.events.update": "تحديث",
"Settings.webhooks.form.events": "الأحداث",
"Settings.webhooks.form.headers": "الرؤوس",
"Settings.webhooks.form.url": "URL",
"Settings.webhooks.headers.remove": "{number} قم بإزالة صف الرأس",
"Settings.webhooks.key": "مفتاح",
"Settings.webhooks.list.button.add": "إنشاء خطاف ويب جديد",
"Settings.webhooks.list.description": "احصل على إخطارات التغييرات POST",
"Settings.webhooks.list.empty.description": "لم يتم العثور على خطافات الويب",
"Settings.webhooks.list.empty.link": "انظر وثائقنا",
"Settings.webhooks.list.empty.title": "لا توجد خطاطيف ويب حتى الان",
"Settings.webhooks.list.th.actions": "أجراءات",
"Settings.webhooks.list.th.status": "حالة",
"Settings.webhooks.singular": "الويب هوك",
"Settings.webhooks.title": "ويب هوك",
"Settings.webhooks.to.delete": "{webhooksToDeleteLength, plural, one {# asset} other {# assets}} selected",
"Settings.webhooks.trigger": "مشغل",
"Settings.webhooks.trigger.cancel": "إلغاء المشغل...",
"Settings.webhooks.trigger.pending": "قيد الانتظار…",
"Settings.webhooks.trigger.save": "يرجى الحفظ للتشغيل",
"Settings.webhooks.trigger.success": "نجاح!",
"Settings.webhooks.trigger.success.label": "نجح الزناد",
"Settings.webhooks.trigger.test": "اختبار الزناد",
"Settings.webhooks.trigger.title": "احفظ قبل تشغيل",
"Settings.webhooks.value": "قيمة",
"skipToContent": "تخطى الى المحتوى",
"submit": "يُقدِّم",
"Usecase.back-end": "المطور الخلفي",
"Usecase.button.skip": "تخطي هذا السؤال",
"Usecase.content-creator": "صانع المحتوى",
"Usecase.front-end": "مطور الواجهة الأمامية",
"Usecase.full-stack": "مطور كامل المكدس",
"Usecase.input.work-type": "ما نوع العمل الذي تفعله؟",
"Usecase.notification.success.project-created": "تم إنشاء المشروع بنجاح",
"Usecase.other": "آخر",
"Usecase.title": "تخبرنا أكثر قليلا عن نفسك",
"Users.components.List.empty": "لا يوجد مستخدمون ...",
"Users.components.List.empty.withFilters": "لا يوجد مستخدمون لديهم عوامل التصفية المطبقة ...",
"Users.components.List.empty.withSearch": "لا يوجد مستخدمون مطابقون للبحث({search})..."
}

View File

@ -826,6 +826,7 @@
"global.back": "Back",
"global.cancel": "Cancel",
"global.change-password": "Change password",
"global.close": "Close",
"global.content-manager": "Content Manager",
"global.continue": "Continue",
"global.delete": "Delete",

View File

@ -57,7 +57,7 @@
"@strapi/provider-audit-logs-local": "4.10.6",
"@strapi/typescript-utils": "4.10.6",
"@strapi/utils": "4.10.6",
"axios": "1.3.4",
"axios": "1.4.0",
"babel-loader": "^9.1.2",
"babel-plugin-styled-components": "2.1.1",
"bcryptjs": "2.4.3",
@ -75,7 +75,7 @@
"fast-deep-equal": "3.1.3",
"find-root": "1.1.0",
"fork-ts-checker-webpack-plugin": "7.3.0",
"formik": "^2.2.6",
"formik": "^2.4.0",
"fractional-indexing": "3.2.0",
"fs-extra": "10.0.0",
"highlight.js": "^10.4.1",
@ -117,7 +117,7 @@
"react-helmet": "^6.1.0",
"react-intl": "6.4.1",
"react-is": "^17.0.2",
"react-query": "3.24.3",
"react-query": "3.39.3",
"react-redux": "8.0.5",
"react-refresh": "0.14.0",
"react-router-dom": "5.3.4",
@ -139,7 +139,7 @@
"yup": "^0.32.9"
},
"devDependencies": {
"@testing-library/dom": "8.19.0",
"@testing-library/dom": "8.20.0",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.4.3",

View File

@ -44,9 +44,9 @@
"lint": "run -T eslint ."
},
"dependencies": {
"axios": "1.3.4",
"axios": "1.4.0",
"date-fns": "2.30.0",
"formik": "^2.2.6",
"formik": "^2.4.0",
"immer": "9.0.19",
"lodash": "4.17.21",
"prop-types": "^15.7.2",

View File

@ -211,7 +211,10 @@ const Notification = ({
) : undefined
}
onClose={handleClose}
closeLabel="Close"
closeLabel={formatMessage({
id: 'global.close',
defaultMessage: 'Close',
})}
title={alertTitle}
variant={variant}
>

View File

@ -59,6 +59,10 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
return options;
},
async wrapResult(result) {
return result;
},
async emitEvent(uid, event, entity) {
// Ignore audit log events to prevent infinite loops
if (uid === 'admin::audit-log') {
@ -83,10 +87,12 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, wrappedParams);
if (kind === 'singleType') {
return db.query(uid).findOne(query);
const entity = db.query(uid).findOne(query);
return this.wrapResult(entity, { uid, action: 'findOne' });
}
return db.query(uid).findMany(query);
const entities = await db.query(uid).findMany(query);
return this.wrapResult(entities, { uid, action: 'findMany' });
},
async findPage(uid, opts) {
@ -94,7 +100,11 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, wrappedParams);
return db.query(uid).findPage(query);
const page = await db.query(uid).findPage(query);
return {
...page,
results: await this.wrapResult(page.results, { uid, action: 'findPage' }),
};
},
// TODO: streamline the logic based on the populate option
@ -103,7 +113,11 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, wrappedParams);
return db.query(uid).findPage(query);
const entities = await db.query(uid).findPage(query);
return {
...entities,
results: await this.wrapResult(entities.results, { uid, action: 'findWithRelationCounts' }),
};
},
async findWithRelationCounts(uid, opts) {
@ -111,7 +125,8 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, wrappedParams);
return db.query(uid).findMany(query);
const entities = await db.query(uid).findMany(query);
return this.wrapResult(entities, { uid, action: 'findWithRelationCounts' });
},
async findOne(uid, entityId, opts) {
@ -119,7 +134,8 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
return db.query(uid).findOne({ ...query, where: { id: entityId } });
const entity = await db.query(uid).findOne({ ...query, where: { id: entityId } });
return this.wrapResult(entity, { uid, action: 'findOne' });
},
async count(uid, opts) {
@ -162,6 +178,8 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
entity = await this.findOne(uid, entity.id, wrappedParams);
}
entity = await this.wrapResult(entity, { uid, action: 'create' });
await this.emitEvent(uid, ENTRY_CREATE, entity);
return entity;
@ -213,6 +231,8 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
entity = await this.findOne(uid, entity.id, wrappedParams);
}
entity = await this.wrapResult(entity, { uid, action: 'update' });
await this.emitEvent(uid, ENTRY_UPDATE, entity);
return entity;
@ -224,7 +244,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
// select / populate
const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
const entityToDelete = await db.query(uid).findOne({
let entityToDelete = await db.query(uid).findOne({
...query,
where: { id: entityId },
});
@ -238,6 +258,8 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
await db.query(uid).delete({ where: { id: entityToDelete.id } });
await deleteComponents(uid, componentsToDelete, { loadComponents: false });
entityToDelete = await this.wrapResult(entityToDelete, { uid, action: 'delete' });
await this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
return entityToDelete;
@ -250,7 +272,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
// select / populate
const query = transformParamsToQuery(uid, wrappedParams);
const entitiesToDelete = await db.query(uid).findMany(query);
let entitiesToDelete = await db.query(uid).findMany(query);
if (!entitiesToDelete.length) {
return null;
@ -265,21 +287,27 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
componentsToDelete.map((compos) => deleteComponents(uid, compos, { loadComponents: false }))
);
entitiesToDelete = await this.wrapResult(entitiesToDelete, { uid, action: 'delete' });
// Trigger webhooks. One for each entity
await Promise.all(entitiesToDelete.map((entity) => this.emitEvent(uid, ENTRY_DELETE, entity)));
return deletedEntities;
},
load(uid, entity, field, params = {}) {
async load(uid, entity, field, params = {}) {
if (!_.isString(field)) {
throw new Error(`Invalid load. Expected "${field}" to be a string`);
}
return db.query(uid).load(entity, field, transformLoadParamsToQuery(uid, field, params));
const loadedEntity = await db
.query(uid)
.load(entity, field, transformLoadParamsToQuery(uid, field, params));
return this.wrapResult(loadedEntity, { uid, field, action: 'load' });
},
loadPages(uid, entity, field, params = {}, pagination = {}) {
async loadPages(uid, entity, field, params = {}, pagination = {}) {
if (!_.isString(field)) {
throw new Error(`Invalid load. Expected "${field}" to be a string`);
}
@ -293,7 +321,12 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformLoadParamsToQuery(uid, field, params, pagination);
return db.query(uid).loadPages(entity, field, query);
const loadedPage = await db.query(uid).loadPages(entity, field, query);
return {
...loadedPage,
results: await this.wrapResult(loadedPage.results, { uid, field, action: 'load' }),
};
},
});

View File

@ -30,11 +30,11 @@
"@strapi/icons": "1.7.9",
"@strapi/provider-upload-local": "4.10.6",
"@strapi/utils": "4.10.6",
"axios": "1.3.4",
"axios": "1.4.0",
"byte-size": "7.0.1",
"cropperjs": "1.5.12",
"date-fns": "2.30.0",
"formik": "2.2.9",
"formik": "2.4.0",
"fs-extra": "10.0.0",
"immer": "9.0.19",
"koa-range": "0.3.0",
@ -46,14 +46,14 @@
"react-dnd": "15.1.2",
"react-helmet": "^6.1.0",
"react-intl": "6.4.1",
"react-query": "3.24.3",
"react-query": "3.39.3",
"react-redux": "8.0.5",
"react-select": "5.7.0",
"sharp": "0.32.0",
"yup": "^0.32.9"
},
"devDependencies": {
"@testing-library/dom": "8.19.0",
"@testing-library/dom": "8.20.0",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.4.3",

View File

@ -47,6 +47,10 @@ describe('Upload plugin bootstrap function', () => {
weeklyMetrics: {
registerCron() {},
},
extensions: {
contentManager: { entityManager: { addSignedFileUrlsToAdmin: jest.fn() } },
core: { entityService: { addSignedFileUrlsToEntityService: jest.fn() } },
},
},
},
},

View File

@ -32,13 +32,6 @@ jest.mock('@strapi/provider-upload-local', () => ({
},
}));
jest.mock('../utils', () => ({
...jest.requireActual('../utils'),
getService: () => ({
contentManager: { entityManager: { addSignedFileUrlsToAdmin: jest.fn() } },
}),
}));
describe('Upload plugin register function', () => {
test('The upload plugin registers the /upload route', async () => {
const registerRoute = jest.fn();

View File

@ -39,6 +39,12 @@ module.exports = async ({ strapi }) => {
await getService('weeklyMetrics').registerCron();
getService('metrics').sendUploadPluginMetrics();
if (strapi.config.get('plugin.upload.signAdminURLsOnly', false)) {
getService('extensions').contentManager.entityManager.addSignedFileUrlsToAdmin();
} else {
getService('extensions').core.entityService.addSignedFileUrlsToEntityService();
}
};
const registerPermissionActions = async () => {

View File

@ -6,7 +6,6 @@ const {
} = require('@strapi/utils');
const _ = require('lodash');
const registerUploadMiddleware = require('./middlewares/upload');
const { getService } = require('./utils');
const spec = require('../documentation/content-api.json');
/**
@ -18,8 +17,6 @@ module.exports = async ({ strapi }) => {
await registerUploadMiddleware({ strapi });
getService('extensions').contentManager.entityManager.addSignedFileUrlsToAdmin();
if (strapi.plugin('graphql')) {
require('./graphql')({ strapi });
}

View File

@ -1,10 +1,9 @@
'use strict';
const { signEntityMedia } = require('../entity-manager');
const { signEntityMedia } = require('../utils');
const { getService } = require('../../../utils');
const { getService } = require('../../../../utils');
jest.mock('../../../../utils');
jest.mock('../../../utils');
describe('Upload | extensions | entity-manager', () => {
const modelUID = 'model';

View File

@ -0,0 +1,39 @@
'use strict';
const { signEntityMedia } = require('../utils');
const addSignedFileUrlsToEntityService = async () => {
const { provider } = strapi.plugins.upload;
const isPrivate = await provider.isPrivate();
// We only need to sign the file urls if the provider is private
if (!isPrivate) {
return;
}
const decorator = (service) => ({
async wrapResult(result, options) {
const wrappedResult = await service.wrapResult.call(this, result, options);
// Load returns only the attribute of the entity, not the entity itself,
if (options.action === 'load') {
const entity = { [options.field]: result };
const signedEntity = await signEntityMedia(entity, options.uid);
return signedEntity[options.field];
}
if (Array.isArray(wrappedResult)) {
return Promise.all(wrappedResult.map((entity) => signEntityMedia(entity, options.uid)));
}
return signEntityMedia(wrappedResult, options.uid);
},
});
strapi.entityService.decorate(decorator);
};
module.exports = {
addSignedFileUrlsToEntityService,
signEntityMedia,
};

View File

@ -0,0 +1,7 @@
'use strict';
const { addSignedFileUrlsToEntityService } = require('./entity-service');
module.exports = {
entityService: { addSignedFileUrlsToEntityService },
};

View File

@ -1,7 +1,9 @@
'use strict';
const contentManagerExtensions = require('./content-manager');
const coreExtensions = require('./core');
module.exports = {
contentManager: contentManagerExtensions,
core: coreExtensions,
};

View File

@ -0,0 +1,51 @@
'use strict';
const { mapAsync, traverseEntity } = require('@strapi/utils');
const { getService } = require('../../utils');
/**
* Visitor function to sign media URLs
* @param {Object} schema
* @param {string} schema.key - The key of the attribute
* @param {string} schema.value - The value of the attribute
* @param {Object} schema.attribute - The attribute definition
* @param {Object} entry
* @param {Function} entry.set - The set function to update the value
*/
const signEntityMediaVisitor = async ({ key, value, attribute }, { set }) => {
const { signFileUrls } = getService('file');
if (!value || attribute.type !== 'media') {
return;
}
// If the attribute is repeatable sign each file
if (attribute.multiple) {
const signedFiles = await mapAsync(value, signFileUrls);
set(key, signedFiles);
return;
}
// If the attribute is not repeatable only sign a single file
const signedFile = await signFileUrls(value);
set(key, signedFile);
};
/**
*
* Iterate through an entity manager result
* Check which modelAttributes are media and pre sign the image URLs
* if they are from the current upload provider
*
* @param {Object} entity
* @param {Object} modelAttributes
* @returns
*/
const signEntityMedia = async (entity, uid) => {
const model = strapi.getModel(uid);
return traverseEntity(signEntityMediaVisitor, { schema: model }, entity);
};
module.exports = {
signEntityMedia,
};

View File

@ -36,7 +36,7 @@
"@strapi/utils": "4.10.6",
"bcryptjs": "2.4.3",
"cheerio": "^1.0.0-rc.12",
"formik": "2.2.9",
"formik": "2.4.0",
"fs-extra": "10.0.0",
"immer": "9.0.19",
"koa-static": "^5.0.0",
@ -45,7 +45,7 @@
"pluralize": "8.0.0",
"react-helmet": "^6.1.0",
"react-intl": "6.4.1",
"react-query": "3.24.3",
"react-query": "3.39.3",
"react-redux": "8.0.5",
"redux": "^4.2.1",
"reselect": "^4.1.7",

View File

@ -34,13 +34,13 @@
"@strapi/helper-plugin": "4.10.6",
"@strapi/icons": "1.7.9",
"@strapi/utils": "4.10.6",
"formik": "2.2.9",
"formik": "2.4.0",
"immer": "9.0.19",
"lodash": "4.17.21",
"prop-types": "^15.7.2",
"qs": "6.11.1",
"react-intl": "6.4.1",
"react-query": "3.24.3",
"react-query": "3.39.3",
"react-redux": "8.0.5",
"redux": "^4.2.1",
"yup": "^0.32.9"

View File

@ -45,6 +45,8 @@ const models = {
'localized-single-type-model': singleTypeModel,
};
const testModels = [['test-model'], ['non-localized-model'], ['localized-single-type-model']];
describe('Entity service decorator', () => {
beforeAll(() => {
global.strapi = {
@ -63,6 +65,9 @@ describe('Entity service decorator', () => {
update() {},
};
},
entityService: {
findOne() {},
},
getModel(uid) {
return models[uid || 'test-model'];
},
@ -136,6 +141,16 @@ describe('Entity service decorator', () => {
['delete', { filters: [{ id: { $in: [1] } }] }],
];
test.each(testModels)('Always uses original wrapParams in output - %s', async (modelName) => {
const defaultService = {
wrapParams: jest.fn(() => Promise.resolve({ Test: 'Test' })),
};
const service = decorator(defaultService);
const output = await service.wrapParams({}, { uid: modelName, action: 'findMany' });
expect(output.Test).toEqual('Test');
});
test.each(testData)(
"Doesn't add locale param when the params contain id or id_in - %s",
async (action, params) => {
@ -321,6 +336,7 @@ describe('Entity service decorator', () => {
const defaultService = {
wrapParams: jest.fn(() => Promise.resolve(entry)),
findMany: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
@ -329,8 +345,7 @@ describe('Entity service decorator', () => {
await service.findMany('test-model', input);
expect(global.strapi.getModel).toHaveBeenCalledWith('test-model');
expect(global.strapi.db.query).toHaveBeenCalledWith('test-model');
expect(findManySpy).toHaveBeenCalled();
expect(defaultService.findMany).toBeCalled();
});
describe('single types', () => {
@ -341,6 +356,7 @@ describe('Entity service decorator', () => {
const defaultService = {
wrapParams: jest.fn(() => Promise.resolve(entry)),
findMany: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
@ -361,12 +377,11 @@ describe('Entity service decorator', () => {
await service.findMany('localized-single-type-model', input);
expect(global.strapi.getModel).toHaveBeenCalledWith('localized-single-type-model');
expect(global.strapi.db.query).toHaveBeenCalledWith('localized-single-type-model');
expect(findManySpy).toHaveBeenCalled();
expect(global.strapi.db.query).toBeCalled();
});
test('calls db.findMany for single type with no local param', async () => {
const findOneSpy = jest.fn();
const findOneSpy = jest.fn(() => Promise.resolve(entry));
const db = {
query: jest.fn(() => ({
findOne: findOneSpy,
@ -381,8 +396,9 @@ describe('Entity service decorator', () => {
await service.findMany('localized-single-type-model', input);
expect(global.strapi.getModel).toHaveBeenCalledWith('localized-single-type-model');
expect(global.strapi.db.query).toHaveBeenCalledWith('localized-single-type-model');
expect(findOneSpy).toHaveBeenCalled();
expect(defaultService.findMany).toHaveBeenCalledWith('localized-single-type-model', {
data: { title: 'title ' },
});
});
});
});

View File

@ -2,7 +2,6 @@
const { has, get, omit, isArray } = require('lodash/fp');
const { ApplicationError } = require('@strapi/utils').errors;
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
const { getService } = require('../utils');
@ -96,7 +95,7 @@ const decorator = (service) => ({
return wrappedParams;
}
return wrapParams(params, ctx);
return wrapParams(wrappedParams, ctx);
},
/**
@ -156,7 +155,7 @@ const decorator = (service) => ({
* @param {string} uid - Model uid
* @param {object} opts - Query options object (params, data, files, populate)
*/
async findMany(uid, opts = {}) {
async findMany(uid, opts) {
const model = strapi.getModel(uid);
const { isLocalizedContentType } = getService('content-types');
@ -167,18 +166,18 @@ const decorator = (service) => ({
const { kind } = strapi.getModel(uid);
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findMany' });
const query = transformParamsToQuery(uid, wrappedParams);
if (kind === 'singleType') {
if (opts[LOCALE_QUERY_FILTER] === 'all') {
return strapi.db.query(uid).findMany(query);
// TODO Fix so this won't break lower lying find many wrappers
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findMany' });
return strapi.db.query(uid).findMany(wrappedParams);
}
return strapi.db.query(uid).findOne(query);
// This one gets transformed into a findOne on a lower layer
return service.findMany.call(this, uid, opts);
}
return strapi.db.query(uid).findMany(query);
return service.findMany.call(this, uid, opts);
},
});

View File

@ -34,7 +34,7 @@
"@strapi/icons": "1.7.9",
"@strapi/utils": "4.10.6",
"bcryptjs": "2.4.3",
"formik": "2.2.9",
"formik": "2.4.0",
"grant-koa": "5.4.8",
"immer": "9.0.19",
"jsonwebtoken": "9.0.0",
@ -45,13 +45,13 @@
"prop-types": "^15.7.2",
"purest": "4.0.2",
"react-intl": "6.4.1",
"react-query": "3.24.3",
"react-query": "3.39.3",
"react-redux": "8.0.5",
"url-join": "4.0.1",
"yup": "^0.32.9"
},
"devDependencies": {
"@testing-library/dom": "8.19.0",
"@testing-library/dom": "8.20.0",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.4.3",

View File

@ -7765,11 +7765,11 @@ __metadata:
"@strapi/provider-audit-logs-local": 4.10.6
"@strapi/typescript-utils": 4.10.6
"@strapi/utils": 4.10.6
"@testing-library/dom": 8.19.0
"@testing-library/dom": 8.20.0
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 8.0.1
"@testing-library/user-event": 14.4.3
axios: 1.3.4
axios: 1.4.0
babel-loader: ^9.1.2
babel-plugin-styled-components: 2.1.1
bcryptjs: 2.4.3
@ -7788,7 +7788,7 @@ __metadata:
fast-deep-equal: 3.1.3
find-root: 1.1.0
fork-ts-checker-webpack-plugin: 7.3.0
formik: ^2.2.6
formik: ^2.4.0
fractional-indexing: 3.2.0
fs-extra: 10.0.0
glob: 8.0.3
@ -7832,7 +7832,7 @@ __metadata:
react-helmet: ^6.1.0
react-intl: 6.4.1
react-is: ^17.0.2
react-query: 3.24.3
react-query: 3.39.3
react-redux: 8.0.5
react-refresh: 0.14.0
react-router-dom: 5.3.4
@ -8028,12 +8028,12 @@ __metadata:
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 8.0.1
"@testing-library/user-event": 14.4.3
axios: 1.3.4
axios: 1.4.0
browserslist-to-esbuild: 1.2.0
cross-env: ^7.0.3
date-fns: 2.30.0
esbuild-loader: ^2.21.0
formik: ^2.2.6
formik: ^2.4.0
history: ^4.9.0
immer: 9.0.19
lodash: 4.17.21
@ -8186,7 +8186,7 @@ __metadata:
"@testing-library/react": 12.1.4
bcryptjs: 2.4.3
cheerio: ^1.0.0-rc.12
formik: 2.2.9
formik: 2.4.0
fs-extra: 10.0.0
history: ^4.9.0
immer: 9.0.19
@ -8199,7 +8199,7 @@ __metadata:
react-dom: ^17.0.2
react-helmet: ^6.1.0
react-intl: 6.4.1
react-query: 3.24.3
react-query: 3.39.3
react-redux: 8.0.5
react-router-dom: 5.3.4
redux: ^4.2.1
@ -8290,7 +8290,7 @@ __metadata:
"@strapi/icons": 1.7.9
"@strapi/utils": 4.10.6
"@testing-library/react": 12.1.4
formik: 2.2.9
formik: 2.4.0
immer: 9.0.19
lodash: 4.17.21
msw: 1.2.1
@ -8299,7 +8299,7 @@ __metadata:
react: ^17.0.2
react-dom: ^17.0.2
react-intl: 6.4.1
react-query: 3.24.3
react-query: 3.39.3
react-redux: 8.0.5
react-router-dom: 5.3.4
redux: ^4.2.1
@ -8343,15 +8343,15 @@ __metadata:
"@strapi/icons": 1.7.9
"@strapi/provider-upload-local": 4.10.6
"@strapi/utils": 4.10.6
"@testing-library/dom": 8.19.0
"@testing-library/dom": 8.20.0
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 8.0.1
"@testing-library/user-event": 14.4.3
axios: 1.3.4
axios: 1.4.0
byte-size: 7.0.1
cropperjs: 1.5.12
date-fns: 2.30.0
formik: 2.2.9
formik: 2.4.0
fs-extra: 10.0.0
immer: 9.0.19
koa-range: 0.3.0
@ -8366,7 +8366,7 @@ __metadata:
react-dom: ^17.0.2
react-helmet: ^6.1.0
react-intl: 6.4.1
react-query: 3.24.3
react-query: 3.39.3
react-redux: 8.0.5
react-router-dom: 5.3.4
react-select: 5.7.0
@ -8389,12 +8389,12 @@ __metadata:
"@strapi/helper-plugin": 4.10.6
"@strapi/icons": 1.7.9
"@strapi/utils": 4.10.6
"@testing-library/dom": 8.19.0
"@testing-library/dom": 8.20.0
"@testing-library/react": 12.1.4
"@testing-library/react-hooks": 8.0.1
"@testing-library/user-event": 14.4.3
bcryptjs: 2.4.3
formik: 2.2.9
formik: 2.4.0
grant-koa: 5.4.8
history: ^4.9.0
immer: 9.0.19
@ -8409,7 +8409,7 @@ __metadata:
react: ^17.0.2
react-dom: ^17.0.2
react-intl: 6.4.1
react-query: 3.24.3
react-query: 3.39.3
react-redux: 8.0.5
react-router-dom: 5.3.4
styled-components: 5.3.3
@ -8853,19 +8853,19 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/dom@npm:8.19.0":
version: 8.19.0
resolution: "@testing-library/dom@npm:8.19.0"
"@testing-library/dom@npm:8.20.0":
version: 8.20.0
resolution: "@testing-library/dom@npm:8.20.0"
dependencies:
"@babel/code-frame": ^7.10.4
"@babel/runtime": ^7.12.5
"@types/aria-query": ^4.2.0
"@types/aria-query": ^5.0.1
aria-query: ^5.0.0
chalk: ^4.1.0
dom-accessibility-api: ^0.5.9
lz-string: ^1.4.4
pretty-format: ^27.0.2
checksum: 6bb93fef96703b6c47cf1b7cc8f71d402a9576084a94ba4e9926f51bd7bb1287fbb4f6942d82bd03fc6f3d998ae97e60f6aea4618f3a1ce6139597d2a4ecb7b9
checksum: 1e599129a2fe91959ce80900a0a4897232b89e2a8e22c1f5950c36d39c97629ea86b4986b60b173b5525a05de33fde1e35836ea597b03de78cc51b122835c6f0
languageName: node
linkType: hard
@ -8998,6 +8998,13 @@ __metadata:
languageName: node
linkType: hard
"@types/aria-query@npm:^5.0.1":
version: 5.0.1
resolution: "@types/aria-query@npm:5.0.1"
checksum: 69fd7cceb6113ed370591aef04b3fd0742e9a1b06dd045c43531448847b85de181495e4566f98e776b37c422a12fd71866e0a1dfd904c5ec3f84d271682901de
languageName: node
linkType: hard
"@types/babel__core@npm:^7.1.14":
version: 7.1.19
resolution: "@types/babel__core@npm:7.1.19"
@ -12021,14 +12028,14 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:1.3.4, axios@npm:^1.0.0":
version: 1.3.4
resolution: "axios@npm:1.3.4"
"axios@npm:1.4.0":
version: 1.4.0
resolution: "axios@npm:1.4.0"
dependencies:
follow-redirects: ^1.15.0
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: 7440edefcf8498bc3cdf39de00443e8101f249972c83b739c6e880d9d669fea9486372dbe8739e88b3bf8bb1ad15f6106693f206f078f4516fe8fd47b1c3093c
checksum: 7fb6a4313bae7f45e89d62c70a800913c303df653f19eafec88e56cea2e3821066b8409bc68be1930ecca80e861c52aa787659df0ffec6ad4d451c7816b9386b
languageName: node
linkType: hard
@ -12041,6 +12048,17 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.0.0":
version: 1.3.4
resolution: "axios@npm:1.3.4"
dependencies:
follow-redirects: ^1.15.0
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: 7440edefcf8498bc3cdf39de00443e8101f249972c83b739c6e880d9d669fea9486372dbe8739e88b3bf8bb1ad15f6106693f206f078f4516fe8fd47b1c3093c
languageName: node
linkType: hard
"axios@npm:^1.3.3":
version: 1.3.5
resolution: "axios@npm:1.3.5"
@ -18027,9 +18045,9 @@ __metadata:
languageName: node
linkType: hard
"formik@npm:2.2.9, formik@npm:^2.2.6":
version: 2.2.9
resolution: "formik@npm:2.2.9"
"formik@npm:2.4.0, formik@npm:^2.4.0":
version: 2.4.0
resolution: "formik@npm:2.4.0"
dependencies:
deepmerge: ^2.1.1
hoist-non-react-statics: ^3.3.0
@ -18040,7 +18058,7 @@ __metadata:
tslib: ^1.10.0
peerDependencies:
react: ">=16.8.0"
checksum: f07f80eee8423b4c5560546c48c4093c47530dae7d931a4e0d947d68ae1aab94291b1bf2e99ecaa5854ee50593b415fb5724c624c787338f0577f066009e8812
checksum: 54ada391af4e59f6439ac3492f66fbe562523de585b47b7169964d7f32b2ed03d8bfe485e7a14bd69ae5e76a77aa8cebd950a7a64822b77822a43b7801096ac2
languageName: node
linkType: hard
@ -28065,21 +28083,21 @@ __metadata:
languageName: node
linkType: hard
"react-query@npm:3.24.3":
version: 3.24.3
resolution: "react-query@npm:3.24.3"
"react-query@npm:3.39.3":
version: 3.39.3
resolution: "react-query@npm:3.39.3"
dependencies:
"@babel/runtime": ^7.5.5
broadcast-channel: ^3.4.1
match-sorter: ^6.0.2
peerDependencies:
react: ^16.8.0 || ^17.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 0b86a33b860c092d36e269db1331afbf0df5b720ea746c9093cd8241204a076ff404affb8b4dae08904e3714545d4bd8920babe2a6e5ee9660d94bbcdbeb8072
checksum: d2de6a0992dbf039ff2de564de1ae6361f8ac7310159dae42ec16f833b79c05caedced187235c42373ac331cc5f2fe9e2b31b14ae75a815e86d86e30ca9887ad
languageName: node
linkType: hard