Skip to content
This repository was archived by the owner on Aug 21, 2024. It is now read-only.

Commit 8851071

Browse files
committed
Merge branch 'dev' into michael/IR-3667_scene_name_too_long
* dev: IR-3602-Changing-CSM-cascade-count-or-tonemapping-spams-errors (#10926) hotfix gizmo zeroed position on rigidbody entities (#10943) Add download progress bar and optimize util files (#10914) [IR-3547] studio: show unsaved changes dialog when switching between scenes (#10921) added tailwind class to break the line in the middle of words to prevent overflowing the alocated space (#10944) Updating the description of hemisphere lights to better explain what they do and why. (#10945) Left aligning the text in the "Add Component" and "Add Entity" menus (#10946) studio: open context menu on file menu right click (#10908) Fixed a bug with project PUTs without commit SHAs (#10942) IR-2873 Fix disabled state for select dropdown arrow (#10950) IR-3700: Added search by sso/email in user table (#10929) IR-3053: Add project-history service (#10736) Component dependencies (#10916) [IR-3324] studio: drag-n-drop folders (#10737) prevent scene from disappearing when double-clicking on a HierarchyTreeNode (#10932) fix tooltip css applied to all popups (#10938)
2 parents aa92c34 + a3efe4d commit 8851071

File tree

83 files changed

+2815
-455
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+2815
-455
lines changed

packages/client-core/i18n/en/admin.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@
8686
"lastUpdatedBy": "Last updated by user id: {{userId}} on {{updatedAt}}",
8787
"fillRequiredFields": "Please fill all required field",
8888
"fixErrorFields": "Please fix all errors",
89-
"logOut": "Log Out"
89+
"logOut": "Log Out",
90+
"newestFirst": "Newest First",
91+
"oldestFirst": "Oldest First"
9092
},
9193
"analytics": {
9294
"loading": "Loading analytics...",
@@ -214,8 +216,10 @@
214216
"repo": "Repo",
215217
"access": "Access",
216218
"invalidateCache": "Invalidate Cache",
217-
"update": "Update"
219+
"update": "Update",
220+
"history": "History"
218221
},
222+
"projectHistory": "Project History",
219223
"addProject": "Add Project",
220224
"updateProject": "Update Project",
221225
"downloadProject": "Download Project",

packages/client-core/i18n/en/editor.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@
568568
},
569569
"hemisphere": {
570570
"name": "Hemisphere Light",
571-
"description": "A light which illuminates the scene from directly overhead.",
571+
"description": "A light which illuminates the scene with a sky color from above and a ground color from below.",
572572
"lbl-skyColor": "Sky Color",
573573
"lbl-groundColor": "Ground Color",
574574
"lbl-intensity": "Intensity"
@@ -1177,7 +1177,7 @@
11771177
"point-light": "A light which emits in all directions from a single point.",
11781178
"spot-light": "Creates a light that shines in a specific direction.",
11791179
"directional-light": "Creates a light that emits evenly in a single direction.",
1180-
"hemisphere-light": "A light which illuminates the scene from directly overhead.",
1180+
"hemisphere-light": "A light which illuminates the scene with a sky color from above and a ground color from below.",
11811181
"particle-system": "Creates a particle emitter.",
11821182
"system": "Inserts code into the scene by creating a new Entity Component System based on the provided .ts file",
11831183
"visual-script": "Customizes state and behavior of entities through a visual node connections.",
@@ -1217,6 +1217,7 @@
12171217
"uploadFiles": "Upload Files",
12181218
"uploadFolder": "Upload Folder",
12191219
"uploadingFiles": "Uploading Files ({{completed}}/{{total}})",
1220+
"downloadingProject": "Downloading Project ({{completed}}/{{total}})",
12201221
"search-placeholder": "Search",
12211222
"generatingThumbnails": "Generating Thumbnails ({{count}} remaining)",
12221223
"file": "File",
@@ -1360,7 +1361,10 @@
13601361
"lbl-thumbnail": "Generate thumbnail & envmap",
13611362
"lbl-confirm": "Save Scene",
13621363
"info-confirm": "Are you sure you want to save the scene?",
1363-
"info-question": "Do you want to save the current scene?"
1364+
"info-question": "Do you want to save the current scene?",
1365+
"unsavedChanges": {
1366+
"title": "Unsaved Changes"
1367+
}
13641368
},
13651369
"saveNewScene": {
13661370
"title": "Save As",
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
CPAL-1.0 License
3+
4+
The contents of this file are subject to the Common Public Attribution License
5+
Version 1.0. (the "License"); you may not use this file except in compliance
6+
with the License. You may obtain a copy of the License at
7+
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE.
8+
The License is based on the Mozilla Public License Version 1.1, but Sections 14
9+
and 15 have been added to cover use of software over a computer network and
10+
provide for limited attribution for the Original Developer. In addition,
11+
Exhibit A has been modified to be consistent with Exhibit B.
12+
13+
Software distributed under the License is distributed on an "AS IS" basis,
14+
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
15+
specific language governing rights and limitations under the License.
16+
17+
The Original Code is Ethereal Engine.
18+
19+
The Original Developer is the Initial Developer. The Initial Developer of the
20+
Original Code is the Ethereal Engine team.
21+
22+
All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023
23+
Ethereal Engine. All Rights Reserved.
24+
*/
25+
26+
import { projectHistoryPath } from '@etherealengine/common/src/schema.type.module'
27+
import { ProjectHistoryType } from '@etherealengine/common/src/schemas/projects/project-history.schema'
28+
import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
29+
30+
import { toDisplayDateTime } from '@etherealengine/common/src/utils/datetime-sql'
31+
import AvatarImage from '@etherealengine/ui/src/primitives/tailwind/AvatarImage'
32+
import Button from '@etherealengine/ui/src/primitives/tailwind/Button'
33+
import { TablePagination } from '@etherealengine/ui/src/primitives/tailwind/Table'
34+
import Text from '@etherealengine/ui/src/primitives/tailwind/Text'
35+
import Tooltip from '@etherealengine/ui/src/primitives/tailwind/Tooltip'
36+
import React from 'react'
37+
import { useTranslation } from 'react-i18next'
38+
import { FaSortAmountDown, FaSortAmountUpAlt } from 'react-icons/fa'
39+
40+
const PROJECT_HISTORY_PAGE_LIMIT = 10
41+
42+
const getRelativeURLFromProject = (projectName: string, url: string) => {
43+
const prefix = `projects/${projectName}/`
44+
if (url.startsWith(prefix)) {
45+
return url.replace(prefix, '')
46+
}
47+
return url
48+
}
49+
50+
const getResourceURL = (projectName: string, url: string, resourceType: 'resource' | 'scene') => {
51+
const relativeURL = getRelativeURLFromProject(projectName, url)
52+
const resourceURL =
53+
resourceType === 'resource'
54+
? `/projects/${projectName}/${relativeURL}`
55+
: `/studio?project=${projectName}&scenePath=${url}`
56+
return {
57+
relativeURL,
58+
resourceURL
59+
}
60+
}
61+
62+
function HistoryLog({ projectHistory, projectName }: { projectHistory: ProjectHistoryType; projectName: string }) {
63+
const { t } = useTranslation()
64+
65+
const RenderAction = () => {
66+
if (projectHistory.action === 'LOCATION_PUBLISHED' || projectHistory.action === 'LOCATION_UNPUBLISHED') {
67+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
68+
locationName: string
69+
sceneURL: string
70+
sceneId: string
71+
}
72+
73+
const verb = projectHistory.action === 'LOCATION_PUBLISHED' ? 'published' : 'unpublished'
74+
75+
const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.sceneURL, 'scene')
76+
77+
return (
78+
<>
79+
<Text>{verb} the location</Text>
80+
81+
{verb === 'published' ? (
82+
<a href={`/location/${actionDetail.locationName}`}>
83+
<Text className="underline-offset-4 hover:underline" fontWeight="semibold">
84+
{actionDetail.locationName}
85+
</Text>
86+
</a>
87+
) : (
88+
<Text className="underline-offset-4 hover:underline" fontWeight="semibold">
89+
{actionDetail.locationName}
90+
</Text>
91+
)}
92+
93+
<Text>from the scene</Text>
94+
95+
<Text href={resourceURL} component="a" className="underline-offset-4 hover:underline" fontWeight="semibold">
96+
{relativeURL}.
97+
</Text>
98+
</>
99+
)
100+
} else if (projectHistory.action === 'LOCATION_MODIFIED') {
101+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
102+
locationName: string
103+
}
104+
105+
return (
106+
<>
107+
<Text>modified the location</Text>
108+
109+
<a href={`/location/${actionDetail.locationName}`}>
110+
<Text className="underline-offset-4 hover:underline" fontWeight="semibold">
111+
{actionDetail.locationName}
112+
</Text>
113+
</a>
114+
</>
115+
)
116+
} else if (projectHistory.action === 'PERMISSION_CREATED' || projectHistory.action === 'PERMISSION_REMOVED') {
117+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
118+
userName: string
119+
userId: string
120+
permissionType: string
121+
}
122+
123+
const verb = projectHistory.action === 'PERMISSION_CREATED' ? 'added' : 'removed'
124+
125+
return (
126+
<>
127+
<Text>{verb} the</Text>
128+
<Text fontWeight="semibold">{actionDetail.permissionType}</Text>
129+
130+
<Text>access to</Text>
131+
132+
<Tooltip content={`UserId: ${actionDetail.userId}`}>
133+
<Text>{actionDetail.userName}</Text>
134+
</Tooltip>
135+
</>
136+
)
137+
} else if (projectHistory.action === 'PERMISSION_MODIFIED') {
138+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
139+
userName: string
140+
userId: string
141+
oldPermissionType: string
142+
newPermissionType: string
143+
}
144+
145+
return (
146+
<>
147+
<Text>updated the permission of the user</Text>
148+
<Tooltip content={`UserId: ${actionDetail.userId}`}>
149+
<Text>{actionDetail.userName}</Text>
150+
</Tooltip>
151+
<Text>from</Text>
152+
<Text fontWeight="semibold">{actionDetail.oldPermissionType}</Text>
153+
<Text>to</Text>
154+
<Text fontWeight="semibold">{actionDetail.newPermissionType}</Text>
155+
</>
156+
)
157+
} else if (projectHistory.action === 'PROJECT_CREATED') {
158+
return <Text>created the project</Text>
159+
} else if (
160+
projectHistory.action === 'RESOURCE_CREATED' ||
161+
projectHistory.action === 'RESOURCE_REMOVED' ||
162+
projectHistory.action === 'SCENE_CREATED' ||
163+
projectHistory.action === 'SCENE_REMOVED'
164+
) {
165+
const verb =
166+
projectHistory.action === 'RESOURCE_CREATED' || projectHistory.action === 'SCENE_CREATED'
167+
? 'created'
168+
: 'removed'
169+
const object =
170+
projectHistory.action === 'RESOURCE_CREATED' || projectHistory.action === 'RESOURCE_REMOVED'
171+
? 'resource'
172+
: 'scene'
173+
174+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
175+
url: string
176+
}
177+
178+
const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.url, object)
179+
180+
return (
181+
<>
182+
<Text>
183+
{verb} the {object}
184+
</Text>
185+
<Text href={resourceURL} component="a" fontWeight="semibold" className="underline-offset-4 hover:underline">
186+
{relativeURL}
187+
</Text>
188+
</>
189+
)
190+
} else if (projectHistory.action === 'RESOURCE_RENAMED' || projectHistory.action === 'SCENE_RENAMED') {
191+
const object = projectHistory.action === 'RESOURCE_RENAMED' ? 'resource' : 'scene'
192+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
193+
oldURL: string
194+
newURL: string
195+
}
196+
197+
const { relativeURL: oldRelativeURL } = getResourceURL(projectName, actionDetail.oldURL, object)
198+
const { relativeURL: newRelativeURL, resourceURL: newResourceURL } = getResourceURL(
199+
projectName,
200+
actionDetail.newURL,
201+
object
202+
)
203+
204+
return (
205+
<>
206+
<Text>renamed a {object} from</Text>
207+
208+
<Text fontWeight="semibold">{oldRelativeURL}</Text>
209+
<Text>to</Text>
210+
<Text
211+
href={newResourceURL}
212+
component="a"
213+
fontWeight="semibold"
214+
className="underline-offset-4 hover:underline"
215+
>
216+
{getRelativeURLFromProject(projectName, newRelativeURL)}
217+
</Text>
218+
</>
219+
)
220+
} else if (projectHistory.action === 'RESOURCE_MODIFIED' || projectHistory.action === 'SCENE_MODIFIED') {
221+
const object = projectHistory.action === 'RESOURCE_MODIFIED' ? 'resource' : 'scene'
222+
const actionDetail = JSON.parse(projectHistory.actionDetail) as {
223+
url: string
224+
}
225+
226+
const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.url, object)
227+
228+
return (
229+
<>
230+
<Text>modified the {object}</Text>
231+
<Text href={resourceURL} component="a" fontWeight="semibold" className="underline-offset-4 hover:underline">
232+
{relativeURL}
233+
</Text>
234+
</>
235+
)
236+
}
237+
238+
return null
239+
}
240+
241+
return (
242+
<div className="mb-3 flex w-full items-center justify-between rounded-lg bg-[#191B1F] px-5 py-2">
243+
<div className="grid grid-flow-col place-items-center gap-x-2 [&>*]:text-nowrap">
244+
<AvatarImage
245+
className="inline-grid min-h-10 min-w-10 rounded-full"
246+
src={projectHistory.userAvatarURL}
247+
name={projectHistory.userName}
248+
/>
249+
250+
<Text className="text-nowrap">{projectHistory.userName}</Text>
251+
252+
<RenderAction />
253+
</div>
254+
255+
<Text className="text-nowrap">{toDisplayDateTime(projectHistory.createdAt)}</Text>
256+
</div>
257+
)
258+
}
259+
260+
export const ProjectHistory = ({ projectId, projectName }: { projectId: string; projectName: string }) => {
261+
const { t } = useTranslation()
262+
const projectHistoryQuery = useFind(projectHistoryPath, {
263+
query: {
264+
projectId: projectId,
265+
$sort: {
266+
createdAt: -1
267+
},
268+
$limit: PROJECT_HISTORY_PAGE_LIMIT
269+
}
270+
})
271+
272+
const sortOrder = projectHistoryQuery.sort.createdAt
273+
274+
const toggleSortOrder = () => {
275+
projectHistoryQuery.setSort({
276+
createdAt: sortOrder === -1 ? 1 : -1
277+
})
278+
}
279+
280+
return (
281+
<div className="w-full flex-row justify-between gap-5 px-2">
282+
<Button
283+
className="mb-4"
284+
onClick={toggleSortOrder}
285+
endIcon={sortOrder === -1 ? <FaSortAmountDown /> : <FaSortAmountUpAlt />}
286+
>
287+
{sortOrder === -1 ? t('admin:components.common.newestFirst') : t('admin:components.common.oldestFirst')}
288+
</Button>
289+
290+
{projectHistoryQuery.data &&
291+
projectHistoryQuery.data.map((projectHistory, index) => (
292+
<HistoryLog key={index} projectHistory={projectHistory} projectName={projectName} />
293+
))}
294+
295+
<TablePagination
296+
totalPages={Math.ceil(projectHistoryQuery.total / projectHistoryQuery.limit)}
297+
currentPage={projectHistoryQuery.page}
298+
onPageChange={(newPage) => projectHistoryQuery.setPage(newPage)}
299+
/>
300+
</div>
301+
)
302+
}

0 commit comments

Comments
 (0)