diff --git a/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx b/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx index 9e6d8b3453..286eac578b 100644 --- a/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx +++ b/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx @@ -64,7 +64,7 @@ export default function ManageUserPermissionModal({ return } try { - await ProjectService.createPermission(userInviteCode.value, project.id) + await ProjectService.createPermission(userInviteCode.value, project.id, 'reviewer') } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } diff --git a/packages/client-core/src/common/services/ProjectService.ts b/packages/client-core/src/common/services/ProjectService.ts index abe046f1a4..909bebc95a 100644 --- a/packages/client-core/src/common/services/ProjectService.ts +++ b/packages/client-core/src/common/services/ProjectService.ts @@ -45,7 +45,8 @@ import { projectPath, projectPermissionPath, ProjectType, - ProjectUpdateParams + ProjectUpdateParams, + UserID } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' import { defineState, getMutableState, useHookstate } from '@etherealengine/hyperflux' @@ -176,11 +177,13 @@ export const ProjectService = { } }, - createPermission: async (userInviteCode: InviteCode, projectId: string) => { + createPermission: async (userInviteCode: InviteCode, projectId: string, type: string) => { try { return Engine.instance.api.service(projectPermissionPath).create({ inviteCode: userInviteCode, - projectId: projectId + userId: '' as UserID, + projectId: projectId, + type }) } catch (err) { logger.error('Error with creating new project-permission', err) diff --git a/packages/common/src/schemas/projects/project-permission.schema.ts b/packages/common/src/schemas/projects/project-permission.schema.ts index 7dc3e12896..09b7ba0748 100644 --- a/packages/common/src/schemas/projects/project-permission.schema.ts +++ b/packages/common/src/schemas/projects/project-permission.schema.ts @@ -47,6 +47,9 @@ export const projectPermissionSchema = Type.Object( userId: TypedString({ format: 'uuid' }), + createdBy: TypedString({ + format: 'uuid' + }), type: Type.String(), user: Type.Ref(userSchema), createdAt: Type.String({ format: 'date-time' }), @@ -59,7 +62,7 @@ export interface ProjectPermissionType extends Static {} // Schema for creating new entries -export const projectPermissionDataProperties = Type.Partial(projectPermissionSchema) +export const projectPermissionDataProperties = Type.Pick(projectPermissionSchema, ['projectId', 'userId', 'type']) export const projectPermissionDataSchema = Type.Intersect( [ @@ -76,7 +79,7 @@ export const projectPermissionDataSchema = Type.Intersect( export interface ProjectPermissionData extends Static {} // Schema for updating existing entries -export const projectPermissionPatchSchema = Type.Partial(projectPermissionSchema, { +export const projectPermissionPatchSchema = Type.Pick(projectPermissionSchema, ['type'], { $id: 'ProjectPermissionPatch' }) export interface ProjectPermissionPatch extends Static {} @@ -86,6 +89,7 @@ export const projectPermissionQueryProperties = Type.Pick(projectPermissionSchem 'id', 'projectId', 'userId', + 'createdBy', 'type' ]) export const projectPermissionQuerySchema = Type.Intersect( diff --git a/packages/editor/src/components/projects/ProjectsPage.tsx b/packages/editor/src/components/projects/ProjectsPage.tsx index 08abc2f5ba..40270dcf6f 100644 --- a/packages/editor/src/components/projects/ProjectsPage.tsx +++ b/packages/editor/src/components/projects/ProjectsPage.tsx @@ -270,7 +270,7 @@ const ProjectsPage = ({ studioPath }: { studioPath: string }) => { } const onCreatePermission = async (userInviteCode: InviteCode, projectId: string) => { - await ProjectService.createPermission(userInviteCode, projectId) + await ProjectService.createPermission(userInviteCode, projectId, 'reviewer') } const onPatchPermission = async (id: string, type: string) => { diff --git a/packages/server-core/src/projects/project-permission/migrations/20240607053537_createdBy-column.ts b/packages/server-core/src/projects/project-permission/migrations/20240607053537_createdBy-column.ts new file mode 100644 index 0000000000..bfab7c962d --- /dev/null +++ b/packages/server-core/src/projects/project-permission/migrations/20240607053537_createdBy-column.ts @@ -0,0 +1,74 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { projectPermissionPath } from '@etherealengine/common/src/schemas/projects/project-permission.schema' +import type { Knex } from 'knex' + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function up(knex: Knex): Promise { + await knex.raw('SET FOREIGN_KEY_CHECKS=0') + + await addCreatedByColumn(knex, projectPermissionPath) + + await knex.raw('SET FOREIGN_KEY_CHECKS=1') +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function down(knex: Knex): Promise { + await knex.raw('SET FOREIGN_KEY_CHECKS=0') + + await dropCreatedByColumn(knex, projectPermissionPath) + + await knex.raw('SET FOREIGN_KEY_CHECKS=1') +} + +export async function addCreatedByColumn(knex: Knex, tableName: string) { + const createdByColumnExists = await knex.schema.hasColumn(tableName, 'createdBy') + + if (createdByColumnExists === false) { + await knex.schema.alterTable(tableName, async (table) => { + //@ts-ignore + table.uuid('createdBy', 36).collate('utf8mb4_bin').nullable().index() + table.foreign('createdBy').references('id').inTable('user').onDelete('CASCADE').onUpdate('CASCADE') + }) + } +} + +export async function dropCreatedByColumn(knex: Knex, tableName: string) { + const createdByColumnExists = await knex.schema.hasColumn(tableName, 'createdBy') + + if (createdByColumnExists === true) { + await knex.schema.alterTable(tableName, async (table) => { + table.dropForeign('createdBy') + table.dropColumn('createdBy') + }) + } +} diff --git a/packages/server-core/src/projects/project-permission/project-permission.hooks.ts b/packages/server-core/src/projects/project-permission/project-permission.hooks.ts index 2b965e08d3..3fc1c83f09 100644 --- a/packages/server-core/src/projects/project-permission/project-permission.hooks.ts +++ b/packages/server-core/src/projects/project-permission/project-permission.hooks.ts @@ -23,11 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { BadRequest, Forbidden } from '@feathersjs/errors' -import { Paginated } from '@feathersjs/feathers' -import { hooks as schemaHooks } from '@feathersjs/schema' -import { disallow, discardQuery, iff, iffElse, isProvider } from 'feathers-hooks-common' - import { INVITE_CODE_REGEX, USER_ID_REGEX } from '@etherealengine/common/src/constants/IdConstants' import { ProjectPermissionData, @@ -40,7 +35,12 @@ import { } from '@etherealengine/common/src/schemas/projects/project-permission.schema' import { projectPath } from '@etherealengine/common/src/schemas/projects/project.schema' import { InviteCode, UserID, UserType, userPath } from '@etherealengine/common/src/schemas/user/user.schema' +import setLoggedInUserData from '@etherealengine/server-core/src/hooks/set-loggedin-user-in-body' import { checkScope } from '@etherealengine/spatial/src/common/functions/checkScope' +import { BadRequest, Forbidden } from '@feathersjs/errors' +import { Paginated } from '@feathersjs/feathers' +import { hooks as schemaHooks } from '@feathersjs/schema' +import { disallow, discardQuery, iff, iffElse, isProvider } from 'feathers-hooks-common' import { HookContext } from '../../../declarations' import logger from '../../ServerLogger' @@ -75,7 +75,7 @@ const ensureInviteCode = async (context: HookContext) } if (data[0].userId && INVITE_CODE_REGEX.test(data[0].userId)) { data[0].inviteCode = data[0].userId as string as InviteCode - delete data[0].userId + delete (data[0] as any).userId } context.data = data[0] } @@ -131,7 +131,7 @@ const checkExistingPermissions = async (context: HookContext) } const data: ProjectPermissionPatch = context.data as ProjectPermissionPatch - context.data = { type: data.type === 'owner' ? 'owner' : 'editor' } + context.data = { type: data.type === 'owner' ? 'owner' : data.type } as ProjectPermissionData } /** @@ -224,6 +224,7 @@ export default { iff(isProvider('external'), verifyProjectOwner()), () => schemaHooks.validateData(projectPermissionDataValidator), schemaHooks.resolveData(projectPermissionDataResolver), + setLoggedInUserData('createdBy'), ensureInviteCode, checkExistingPermissions ], diff --git a/packages/server-core/src/projects/project-permission/project-permission.test.ts b/packages/server-core/src/projects/project-permission/project-permission.test.ts index 3a0320d27c..93f719d9a2 100644 --- a/packages/server-core/src/projects/project-permission/project-permission.test.ts +++ b/packages/server-core/src/projects/project-permission/project-permission.test.ts @@ -196,7 +196,8 @@ describe('project-permission.test', () => { project1Permission2 = await app.service(projectPermissionPath).create( { projectId: project1.id, - userId: user2.id + userId: user2.id, + type: 'editor' }, params ) @@ -216,7 +217,8 @@ describe('project-permission.test', () => { const duplicate = await app.service(projectPermissionPath).create( { projectId: project1.id, - userId: user2.id + userId: user2.id, + type: 'editor' }, params ) @@ -237,7 +239,8 @@ describe('project-permission.test', () => { await app.service(projectPermissionPath).create( { projectId: 'abcdefg', - userId: user2.id + userId: user2.id, + type: 'editor' }, params ) @@ -259,7 +262,8 @@ describe('project-permission.test', () => { await app.service(projectPermissionPath).create( { projectId: project1.id, - userId: 'abcdefg' as UserID + userId: 'abcdefg' as UserID, + type: 'editor' }, params ) @@ -281,7 +285,8 @@ describe('project-permission.test', () => { const res = await app.service(projectPermissionPath).create( { projectId: project1.id, - userId: user3.id + userId: user3.id, + type: 'editor' }, params ) @@ -303,7 +308,8 @@ describe('project-permission.test', () => { await app.service(projectPermissionPath).create( { projectId: project1.id, - userId: user3.id + userId: user3.id, + type: 'editor' }, params ) @@ -353,8 +359,6 @@ describe('project-permission.test', () => { const update = (await app.service(projectPermissionPath).patch( project1Permission2.id, { - projectId: project1.id, - userId: 'abcdefg' as UserID, type: 'owner' } // params @@ -374,8 +378,6 @@ describe('project-permission.test', () => { const update = (await app.service(projectPermissionPath).patch( project1Permission2.id, { - projectId: project1.id, - userId: user2.id, type: 'editor' }, params @@ -397,8 +399,6 @@ describe('project-permission.test', () => { await app.service(projectPermissionPath).patch( project1Permission2.id, { - projectId: project1.id, - userId: user3.id, type: '' }, params @@ -427,8 +427,6 @@ describe('project-permission.test', () => { await app.service(projectPermissionPath).patch( project1Permission2.id, { - projectId: project1.id, - userId: user3.id, type: '' }, params diff --git a/packages/ui/src/primitives/tailwind/Autocomplete/index.tsx b/packages/ui/src/primitives/tailwind/Autocomplete/index.tsx new file mode 100644 index 0000000000..711e4188cc --- /dev/null +++ b/packages/ui/src/primitives/tailwind/Autocomplete/index.tsx @@ -0,0 +1,63 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import React from 'react' +import Input from '../Input' + +export type AutoCompleteOptionsType = { label: string; value: any } + +export interface AutoCompleteProps { + value: string + options: AutoCompleteOptionsType[] + className?: string + placeholder?: string + onSelect: (value: any) => void + onChange: (e: React.ChangeEvent) => void +} + +const AutoComplete = ({ options, onSelect, placeholder, className, value, onChange }: AutoCompleteProps) => { + return ( +
+ + {options.length > 0 && ( +
+
    + {options.map((option, index) => ( +
  • onSelect(option.value)} + > + {option.label} +
  • + ))} +
+
+ )} +
+ ) +} + +export default AutoComplete diff --git a/packages/ui/src/primitives/tailwind/Radio/index.tsx b/packages/ui/src/primitives/tailwind/Radio/index.tsx index 565b8d6dc0..9c3e8abbae 100644 --- a/packages/ui/src/primitives/tailwind/Radio/index.tsx +++ b/packages/ui/src/primitives/tailwind/Radio/index.tsx @@ -32,7 +32,8 @@ export const RadioRoot = ({ onChange, selected, className, - disabled + disabled, + description }: { label: string value: string | number @@ -40,26 +41,30 @@ export const RadioRoot = ({ selected: boolean className?: string disabled?: boolean + description?: string }) => { - const twClassname = twMerge('flex items-center', className) + const twClassname = twMerge('flex flex-col gap-2', className) return (
- - +
+ + +
+ {description &&
{description}
}
) } @@ -75,7 +80,7 @@ const Radio = ({ disabled }: { value: T - options: { label: string; value: T }[] + options: { label: string; value: T; description?: string }[] onChange: (value: T) => void className?: string horizontal?: boolean @@ -83,7 +88,7 @@ const Radio = ({ }) => { return (
- {options.map(({ label, value: optionValue }) => ( + {options.map(({ label, value: optionValue, description }) => ( ({ value={optionValue} onChange={(event) => onChange(event.target.value as T)} disabled={disabled} + description={description} /> ))}