diff --git a/packages/api-apw/__tests__/graphql/reviewersGroup.crud.test.ts b/packages/api-apw/__tests__/graphql/reviewersGroup.crud.test.ts new file mode 100644 index 00000000000..ecdeacea530 --- /dev/null +++ b/packages/api-apw/__tests__/graphql/reviewersGroup.crud.test.ts @@ -0,0 +1,487 @@ +import { useGraphQlHandler } from "~tests/utils/useGraphQlHandler"; +import { defaultIdentity } from "../utils/defaultIdentity"; + +const identityRoot = { + id: "root", + displayName: "root", + type: "admin", + email: "root@webiny.com" +}; +const updatedDisplayName = "Robert Downey"; + +describe("Reviewer crud test", () => { + const { securityIdentity, reviewer, until } = useGraphQlHandler({ + path: "/graphql", + plugins: [defaultIdentity()] + }); + + const { securityIdentity: securityIdentityRoot } = useGraphQlHandler({ + path: "/graphql", + plugins: [defaultIdentity()], + identity: identityRoot + }); + + const { securityIdentity: securityIdentityRootUpdated } = useGraphQlHandler({ + path: "/graphql", + plugins: [defaultIdentity()], + identity: { + ...identityRoot, + displayName: updatedDisplayName + } + }); + + it("should be able to hook on to after login", async () => { + const [response] = await securityIdentity.login(); + expect(response).toMatchObject({ + data: { + security: { + login: { + data: { + id: "12345678" + }, + error: null + } + } + } + }); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 1; + }, + { + name: "Wait for listReviewers query" + } + ); + + /** + * Should created a reviewer entry after login. + */ + const [listReviewersResponse] = await reviewer.listReviewersQuery({}); + expect(listReviewersResponse).toEqual({ + data: { + apw: { + listReviewers: { + data: [ + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: "12345678", + displayName: "John Doe", + type: "admin" + }, + identityId: "12345678", + displayName: "John Doe", + type: "admin", + email: "testing@webiny.com" + } + ], + error: null, + meta: { + hasMoreItems: false, + cursor: null, + totalCount: 1 + } + } + } + } + }); + /* + * Login with another identity. + */ + await securityIdentityRoot.login(); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 2; + }, + { + name: "Wait for listReviewers query" + } + ); + + /** + * Should now have 2 reviewers. + */ + const [listReviewersAgainResponse] = await reviewer.listReviewersQuery({}); + expect(listReviewersAgainResponse).toEqual({ + data: { + apw: { + listReviewers: { + data: [ + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: identityRoot.id, + displayName: identityRoot.displayName, + type: identityRoot.type + }, + identityId: identityRoot.id, + displayName: identityRoot.displayName, + type: "admin", + email: identityRoot.email + }, + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: "12345678", + displayName: "John Doe", + type: "admin" + }, + identityId: "12345678", + displayName: "John Doe", + type: "admin", + email: "testing@webiny.com" + } + ], + error: null, + meta: { + hasMoreItems: false, + cursor: null, + totalCount: 2 + } + } + } + } + }); + }); + + it("should not create more than one entry due to multiple login", async () => { + const [response] = await securityIdentity.login(); + expect(response).toMatchObject({ + data: { + security: { + login: { + data: { + id: "12345678" + }, + error: null + } + } + } + }); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 1; + }, + { + name: "Wait for listReviewers query" + } + ); + + /** + * Should created a reviewer entry after login. + */ + const [listReviewersResponse] = await reviewer.listReviewersQuery({}); + expect(listReviewersResponse).toEqual({ + data: { + apw: { + listReviewers: { + data: [ + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: "12345678", + displayName: "John Doe", + type: "admin" + }, + identityId: "12345678", + displayName: "John Doe", + type: "admin", + email: "testing@webiny.com" + } + ], + error: null, + meta: { + hasMoreItems: false, + cursor: null, + totalCount: 1 + } + } + } + } + }); + /* + * Login again with same identity. + */ + await securityIdentity.login(); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 1; + }, + { + name: "Wait for listReviewers query" + } + ); + + /* + * Should not have 2 reviewers. + */ + const [listReviewersAgainResponse] = await reviewer.listReviewersQuery({}); + expect(listReviewersAgainResponse).toEqual({ + data: { + apw: { + listReviewers: { + data: [ + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: "12345678", + displayName: "John Doe", + type: "admin" + }, + identityId: "12345678", + displayName: "John Doe", + type: "admin", + email: "testing@webiny.com" + } + ], + error: null, + meta: { + hasMoreItems: false, + cursor: null, + totalCount: 1 + } + } + } + } + }); + }); + + it(`should update "displayName" after login if identity has been updated`, async () => { + const [response] = await securityIdentityRoot.login(); + expect(response).toMatchObject({ + data: { + security: { + login: { + data: { + id: "root" + }, + error: null + } + } + } + }); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 1; + }, + { + name: "Wait for listReviewers query" + } + ); + + /** + * Should created a reviewer entry after login. + */ + const [listReviewersResponse] = await reviewer.listReviewersQuery({}); + expect(listReviewersResponse).toEqual({ + data: { + apw: { + listReviewers: { + data: [ + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: "root", + displayName: "root", + type: "admin" + }, + identityId: "root", + displayName: "root", + type: "admin", + email: identityRoot.email + } + ], + error: null, + meta: { + hasMoreItems: false, + cursor: null, + totalCount: 1 + } + } + } + } + }); + /* + * Login again with same identity. + */ + await securityIdentityRootUpdated.login(); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 1; + }, + { + name: "Wait for listReviewers query" + } + ); + + /* + * Should not have 2 reviewers. + */ + const [listReviewersAgainResponse] = await reviewer.listReviewersQuery({}); + expect(listReviewersAgainResponse).toEqual({ + data: { + apw: { + listReviewers: { + data: [ + { + id: expect.any(String), + createdOn: expect.stringMatching(/^20/), + savedOn: expect.stringMatching(/^20/), + createdBy: { + id: "root", + displayName: "root", + type: "admin" + }, + identityId: "root", + displayName: updatedDisplayName, + type: "admin", + email: identityRoot.email + } + ], + error: null, + meta: { + hasMoreItems: false, + cursor: null, + totalCount: 1 + } + } + } + } + }); + }); + + it("should update reviewer when login info changes", async () => { + const { securityIdentity: baseSecurityIdentity, reviewer } = useGraphQlHandler({ + path: "/graphql", + identity: { + id: "mockUpdateIdentityId", + type: "admin", + displayName: "Base Identity" + } + }); + await securityIdentity.login(); + await baseSecurityIdentity.login(); + + await until( + () => reviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data; + return list.length === 2; + }, + { + name: "Wait for listReviewers query" + } + ); + + const [response] = await reviewer.listReviewersQuery({}); + + expect(response).toMatchObject({ + data: { + apw: { + listReviewers: { + data: [ + { + identityId: "mockUpdateIdentityId" + }, + { + identityId: "12345678" + } + ], + error: null, + meta: { + totalCount: 2, + hasMoreItems: false, + cursor: null + } + } + } + } + }); + + const email = "mock@webiny.local"; + + const { securityIdentity: updatedSecurityIdentity, reviewer: updatedReviewer } = + useGraphQlHandler({ + path: "/graphql", + identity: { + id: "mockUpdateIdentityId", + type: "admin", + displayName: "Base Identity", + email + }, + permissions: [] + }); + + await updatedSecurityIdentity.login(); + + await until( + () => updatedReviewer.listReviewersQuery({}).then(([data]) => data), + (response: any) => { + const list = response.data.apw.listReviewers.data as any[]; + if (list.length !== 2) { + return false; + } + return list.some(reviewer => reviewer.email === email); + }, + { + name: "Wait for listReviewers query after updated login" + } + ); + + const [responseAfterUpdatedLogin] = await reviewer.listReviewersQuery({}); + + expect(responseAfterUpdatedLogin).toMatchObject({ + data: { + apw: { + listReviewers: { + data: [ + { + identityId: "mockUpdateIdentityId", + email + }, + { + identityId: "12345678" + } + ], + error: null, + meta: { + totalCount: 2, + hasMoreItems: false, + cursor: null + } + } + } + } + }); + }); +}); diff --git a/packages/api-apw/src/crud/createContentReviewMethods.ts b/packages/api-apw/src/crud/createContentReviewMethods.ts index ce27eedba5d..7f13ffc8db8 100644 --- a/packages/api-apw/src/crud/createContentReviewMethods.ts +++ b/packages/api-apw/src/crud/createContentReviewMethods.ts @@ -8,6 +8,7 @@ import { ApwContentReviewStatus, ApwContentReviewStepStatus, ApwReviewerCrud, + ApwReviewsGroupCrud, ApwScheduleActionData, ApwWorkflowStepTypes, CreateApwContentReviewParams, @@ -41,6 +42,7 @@ import { PluginsContainer } from "@webiny/plugins"; export interface CreateContentReviewMethodsParams extends CreateApwParams { getReviewer: ApwReviewerCrud["get"]; + getReviewersGroup: ApwReviewsGroupCrud["get"]; getContentGetter: AdvancedPublishingWorkflow["getContentGetter"]; getContentPublisher: AdvancedPublishingWorkflow["getContentPublisher"]; getContentUnPublisher: AdvancedPublishingWorkflow["getContentUnPublisher"]; diff --git a/packages/api-apw/src/crud/createReviewersGroupMethods.ts b/packages/api-apw/src/crud/createReviewersGroupMethods.ts index 573958474af..20368de8814 100644 --- a/packages/api-apw/src/crud/createReviewersGroupMethods.ts +++ b/packages/api-apw/src/crud/createReviewersGroupMethods.ts @@ -1,3 +1,43 @@ -export function createReviewersGroupMethods(): Record { - return {}; +import { + ApwReviewersGroup, + ApwReviewsGroupCrud, + CreateApwParams, + CreateApwReviewsGroupParams, + OnReviewerGroupAfterCreateTopicParams, + OnReviewerGroupBeforeCreateTopicParams, + UpdateApwReviewsGroupParams +} from "~/types"; +import { createTopic } from "@webiny/pubsub"; + +export function createReviewersGroupMethods({ + storageOperations +}: CreateApwParams): ApwReviewsGroupCrud { + // create + const onReviewersGroupBeforeCreate = createTopic( + "apw.onReviewersGroupBeforeCreate" + ); + const onReviewersGroupAfterCreate = createTopic( + "apw.onReviewersGroupAfterCreate" + ); + + return { + onReviewersGroupBeforeCreate, + onReviewersGroupAfterCreate, + async create(data: CreateApwReviewsGroupParams): Promise { + await onReviewersGroupBeforeCreate.publish(data); + + const reviewersGroup = await storageOperations.createReviewersGroup({ data }); + + await onReviewersGroupAfterCreate.publish({ group: reviewersGroup }); + + return reviewersGroup; + }, + async get(id: string): Promise { + return new Promise(); + }, + async update(id: string, data: UpdateApwReviewsGroupParams): Promise { + return null; + }, + async delete(id: string): Promise {} + }; } diff --git a/packages/api-apw/src/storageOperations/index.ts b/packages/api-apw/src/storageOperations/index.ts index 05f28fce9f9..302c452b9a1 100644 --- a/packages/api-apw/src/storageOperations/index.ts +++ b/packages/api-apw/src/storageOperations/index.ts @@ -8,6 +8,7 @@ import { createChangeRequestStorageOperations } from "./changeRequestStorageOper import { createCommentStorageOperations } from "~/storageOperations/commentStorageOperations"; import { createApwModels } from "./models"; import { Security } from "@webiny/api-security/types"; +import { createReviewersGroupStorageOperations } from "~/storageOperations/reviewsGroupStorageOperations"; export interface CreateApwStorageOperationsParams { cms: HeadlessCms; @@ -15,6 +16,12 @@ export interface CreateApwStorageOperationsParams { getCmsContext: () => CmsContext; } +export interface CreateApwReviewersGroupStorageOperationsParams { + cms: HeadlessCms; + security: Security; + getCmsContext: () => CmsContext; +} + /** * Using any because value can be a lot of types. * TODO @ts-refactor figure out correct types. @@ -36,6 +43,7 @@ export const createStorageOperations = ( return { ...createReviewerStorageOperations(params), + ...createReviewersGroupStorageOperations(params), ...createWorkflowStorageOperations(params), ...createContentReviewStorageOperations(params), ...createChangeRequestStorageOperations(params), diff --git a/packages/api-apw/src/storageOperations/models/index.ts b/packages/api-apw/src/storageOperations/models/index.ts index 2ac358cdba5..cbcce5f9b86 100644 --- a/packages/api-apw/src/storageOperations/models/index.ts +++ b/packages/api-apw/src/storageOperations/models/index.ts @@ -8,6 +8,7 @@ import { createCommentModelDefinition } from "./comment.model"; import { createChangeRequestModelDefinition } from "./changeRequest.model"; import { CmsContext } from "@webiny/api-headless-cms/types"; import { isInstallationPending } from "~/plugins/utils"; +import { createReviewersGroupModelDefinition } from "~/storageOperations/models/reviewersGroup.model"; export const createApwModels = (context: CmsContext) => { /** @@ -52,6 +53,7 @@ export const createApwModels = (context: CmsContext) => { */ const changeRequestModelDefinition = createChangeRequestModelDefinition(); const reviewerModelDefinition = createReviewerModelDefinition(); + const reviewersGroupDefinition = createReviewersGroupModelDefinition(); const workflowModelDefinition = createWorkflowModelDefinition({ reviewerModelId: reviewerModelDefinition.modelId }); @@ -66,6 +68,7 @@ export const createApwModels = (context: CmsContext) => { workflowModelDefinition, contentReviewModelDefinition, reviewerModelDefinition, + reviewersGroupDefinition, changeRequestModelDefinition, commentModelDefinition ]; diff --git a/packages/api-apw/src/storageOperations/models/reviewersGroup.model.ts b/packages/api-apw/src/storageOperations/models/reviewersGroup.model.ts new file mode 100644 index 00000000000..b2ad2cdf7e5 --- /dev/null +++ b/packages/api-apw/src/storageOperations/models/reviewersGroup.model.ts @@ -0,0 +1,80 @@ +import { createModelField } from "./utils"; +import { WorkflowModelDefinition } from "~/types"; + +const idField = () => + createModelField({ + label: "Identity Id", + type: "text", + parent: "reviewersGroup", + validation: [ + { + message: "`identityId` field value is required in reviewersGroup.", + name: "required" + } + ] + }); + +const nameField = () => + createModelField({ + label: "Name", + type: "text", + parent: "reviewersGroup", + validation: [ + { + message: "`name` field value is required in reviewersGroup.", + name: "required" + } + ] + }); + +const slugField = () => + createModelField({ + label: "Slug", + type: "text", + parent: "reviewersGroup", + validation: [ + { + message: "`slug` field value is required in reviewersGroup.", + name: "required" + } + ] + }); + +const descriptionField = () => + createModelField({ + label: "Description", + type: "text", + parent: "reviewersGroup" + }); + +const reviewersField = () => + createModelField({ + label: "Reviewers", + type: "object", + parent: "reviewersGroup", + validation: [ + { + message: "`reviewers` field value is required in reviewersGroup.", + name: "required" + } + ] + }); + +export const REVIEWERS_GROUP_MODEL_ID = "apwReviewersGroupModelDefinition"; + +export const createReviewersGroupModelDefinition = (): WorkflowModelDefinition => { + return { + name: "APW - Reviewer", + modelId: REVIEWERS_GROUP_MODEL_ID, + titleFieldId: "name", + layout: [ + ["reviewer_identityId"], + ["reviewersGroup_name"], + ["reviewersGroup_description"], + ["reviewersGroup_reviewers"] + ], + fields: [idField(), nameField(), slugField(), descriptionField(), reviewersField()], + description: "", + isPrivate: true + }; +}; diff --git a/packages/api-apw/src/storageOperations/reviewsGroupStorageOperations.ts b/packages/api-apw/src/storageOperations/reviewsGroupStorageOperations.ts new file mode 100644 index 00000000000..61f1b05f3a6 --- /dev/null +++ b/packages/api-apw/src/storageOperations/reviewsGroupStorageOperations.ts @@ -0,0 +1,36 @@ +import { ApwReviewersGroupStorageOperations } from "./types"; +import { + baseFields, + CreateApwReviewersGroupStorageOperationsParams, + getFieldValues +} from "~/storageOperations/index"; +import WebinyError from "@webiny/error"; + +export const createReviewersGroupStorageOperations = ({ + cms, + security +}: CreateApwReviewersGroupStorageOperationsParams): ApwReviewersGroupStorageOperations => { + const getReviewersGroupModel = async () => { + const model = await security.withoutAuthorization(async () => { + return cms.getModel("apwReviewersGroupModelDefinition"); + }); + if (!model) { + throw new WebinyError( + "Could not find `apwReviewersGroupModelDefinition` model.", + "MODEL_NOT_FOUND_ERROR" + ); + } + return model; + }; + + return { + getReviewersGroupModel, + async createReviewersGroup(params) { + const model = await getReviewersGroupModel(); + const entry = await security.withoutAuthorization(async () => { + return cms.createEntry(model, params.data); + }); + return getFieldValues(entry, baseFields); + } + }; +}; diff --git a/packages/api-apw/src/storageOperations/types.ts b/packages/api-apw/src/storageOperations/types.ts index 17f6a8dcfc8..b1774497949 100644 --- a/packages/api-apw/src/storageOperations/types.ts +++ b/packages/api-apw/src/storageOperations/types.ts @@ -1,10 +1,11 @@ import { CmsModel } from "@webiny/api-headless-cms/types"; import { - ApwReviewerStorageOperations as BaseApwReviewerStorageOperations, - ApwWorkflowStorageOperations as BaseApwWorkflowStorageOperations, - ApwContentReviewStorageOperations as BaseApwContentReviewStorageOperations, ApwChangeRequestStorageOperations as BaseApwChangeRequestStorageOperations, - ApwCommentStorageOperations as BaseApwCommentStorageOperations + ApwCommentStorageOperations as BaseApwCommentStorageOperations, + ApwContentReviewStorageOperations as BaseApwContentReviewStorageOperations, + ApwReviewersGroupStorageOperations as BaseApwReviewersGroupStorageOperations, + ApwReviewerStorageOperations as BaseApwReviewerStorageOperations, + ApwWorkflowStorageOperations as BaseApwWorkflowStorageOperations } from "~/types"; export interface ApwCommentStorageOperations extends BaseApwCommentStorageOperations { @@ -21,6 +22,13 @@ export interface ApwReviewerStorageOperations extends BaseApwReviewerStorageOper getReviewerModel(): Promise; } +export interface ApwReviewersGroupStorageOperations extends BaseApwReviewersGroupStorageOperations { + /** + * @internal + */ + getReviewersGroupModel(): Promise; +} + export interface ApwWorkflowStorageOperations extends BaseApwWorkflowStorageOperations { /** * @internal diff --git a/packages/api-apw/src/types.ts b/packages/api-apw/src/types.ts index ba6dd054849..a82da86df5f 100644 --- a/packages/api-apw/src/types.ts +++ b/packages/api-apw/src/types.ts @@ -1,15 +1,15 @@ import { CmsEntry as BaseCmsEntry, - OnEntryBeforePublishTopicParams, OnEntryAfterPublishTopicParams, - OnEntryAfterUnpublishTopicParams + OnEntryAfterUnpublishTopicParams, + OnEntryBeforePublishTopicParams } from "@webiny/api-headless-cms/types"; import { - Page, - OnPageBeforeCreateTopicParams, OnPageBeforeCreateFromTopicParams, - OnPageBeforeUpdateTopicParams, + OnPageBeforeCreateTopicParams, OnPageBeforePublishTopicParams, + OnPageBeforeUpdateTopicParams, + Page, PageSettings } from "@webiny/api-page-builder/types"; import { Context } from "@webiny/api/types"; @@ -163,6 +163,13 @@ export interface ApwReviewerWithEmail extends Omit { email: string; } +export interface ApwReviewersGroup extends ApwBaseFields { + name: string; + slug: string; + description?: string; + reviewers: ApwReviewer[]; +} + export interface ApwComment extends ApwBaseFields { body: Record; changeRequest: string; @@ -334,6 +341,21 @@ export interface UpdateApwContentReviewParams { content?: ApwContentReviewContent; } +export interface CreateApwReviewsGroupParams { + name: string; + slug: string; + description?: string; + reviewers: ApwReviewer[]; +} + +export interface UpdateApwReviewsGroupParams { + groupId: string; + name: string; + slug: string; + description?: string; + reviewers: ApwReviewer[]; +} + interface BaseApwCrud { get(id: string): Promise; @@ -465,6 +487,19 @@ export interface ApwContentReviewCrud onContentReviewBeforeList: Topic; } +export interface ApwReviewsGroupCrud + extends BaseApwCrud< + ApwReviewersGroup, + CreateApwReviewsGroupParams, + any //UpdateApwReviewsGroupParams + > { + /** + * Lifecycle events + */ + onReviewersGroupBeforeCreate: Topic; + onReviewersGroupAfterCreate: Topic; +} + export type ContentGetter = ( id: string, settings: { modelId?: string } @@ -552,6 +587,17 @@ interface StorageOperationsDeleteReviewerParams { id: string; } +interface CreateApwReviewersGroupData { + name: string; + slug: string; + description?: string; + reviewers: ApwReviewer[]; +} + +interface StorageOperationsCreateReviewersGroupParams { + data: CreateApwReviewersGroupData; +} + interface StorageOperationsGetParams { id: string; } @@ -642,6 +688,12 @@ export interface ApwReviewerStorageOperations { deleteReviewer(params: StorageOperationsDeleteReviewerParams): Promise; } +export interface ApwReviewersGroupStorageOperations { + createReviewersGroup( + params: StorageOperationsCreateReviewersGroupParams + ): Promise; +} + export interface ApwWorkflowStorageOperations { /* * Workflow methods @@ -716,6 +768,7 @@ export interface ApwCommentStorageOperations { export interface ApwStorageOperations extends ApwReviewerStorageOperations, + ApwReviewersGroupStorageOperations, ApwWorkflowStorageOperations, ApwContentReviewStorageOperations, ApwChangeRequestStorageOperations, @@ -959,6 +1012,23 @@ export interface OnWorkflowAfterDeleteTopicParams { export type WorkflowModelDefinition = Omit; +/** + * @category Lifecycle events + */ +export interface OnReviewerGroupBeforeCreateTopicParams { + name: string; + slug: string; + description?: string; + reviewers: ApwReviewer[]; +} + +/** + * @category Lifecycle events + */ +export interface OnReviewerGroupAfterCreateTopicParams { + group: ApwReviewersGroup; +} + /** * Headless CMS */ diff --git a/yarn.lock b/yarn.lock index 126c451aac1..f00a21f46c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16170,6 +16170,16 @@ __metadata: languageName: unknown linkType: soft +"@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser": + version: 0.0.0-use.local + resolution: "@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser" + dependencies: + "@webiny/cli": 0.0.0 + "@webiny/lexical-editor": 0.0.0 + "@webiny/project-utils": 0.0.0 + languageName: unknown + linkType: soft + "@webiny/i18n-react@0.0.0, @webiny/i18n-react@workspace:packages/i18n-react": version: 0.0.0-use.local resolution: "@webiny/i18n-react@workspace:packages/i18n-react"