Skip to content

Commit 08b3d8e

Browse files
authored
feat: introduce entity versioning (DAP-4797) (#40)
* refactor: make object parameter in getItem * refactor: make object parameter in getItem * feat: getting versioning data * feat: provide backward compatibility * fix: empty icons * feat: name mapping (open_with) * feat: saving versioned items * feat: show version number * feat: mutation version switcher ui * fix: selected mutation version are not loaded * feat: update mutation when version was switched * fix: mutation is not refreshed after editing * fix: editing of previous versions overrides next one
1 parent da8e50b commit 08b3d8e

28 files changed

+520
-91
lines changed

apps/extension/src/contentscript/multitable-panel/components/dropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export const Dropdown: FC<DropdownProps> = ({
187187
) : selectedMutation.source === EntitySourceType.Local ? (
188188
<Badge margin="0 4px 0 0" text={selectedMutation.source} theme="white" />
189189
) : null}
190-
{selectedMutation.metadata.name}
190+
{selectedMutation.metadata.name} (v{selectedMutation.version})
191191
</SelectedMutationDescription>
192192
{selectedMutation.authorId ? (
193193
<SelectedMutationId>by {selectedMutation.authorId}</SelectedMutationId>

apps/extension/src/contentscript/multitable-panel/components/mutation-editor-modal.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { AppInMutation } from '@mweb/backend'
1313
import { Image } from './image'
1414
import { useSaveMutation, useMutableWeb } from '@mweb/engine'
1515
import { ButtonsGroup } from './buttons-group'
16+
import { useMutationVersions } from '@mweb/engine'
17+
import { MutationVersionDropdown } from './mutation-version-dropdown'
1618

1719
const SelectedMutationEditorWrapper = styled.div`
1820
display: flex;
@@ -197,6 +199,7 @@ const EMPTY_MUTATION_ID = '/mutation/NewMutation'
197199
const createEmptyMutation = (): MutationDto => ({
198200
authorId: null,
199201
blockNumber: 0,
202+
version: '0',
200203
id: EMPTY_MUTATION_ID,
201204
localId: 'NewMutation',
202205
timestamp: 0,
@@ -254,7 +257,7 @@ const alerts: { [name: string]: IAlert } = {
254257
}
255258

256259
export const MutationEditorModal: FC<Props> = ({ apps, baseMutation, localMutations, onClose }) => {
257-
const { switchMutation, switchPreferredSource } = useMutableWeb()
260+
const { switchMutation, switchPreferredSource, isLoading } = useMutableWeb()
258261
const loggedInAccountId = useAccountId()
259262
const [isModified, setIsModified] = useState(true)
260263
const [appIdToOpenDocsModal, setAppIdToOpenDocsModal] = useState<string | null>(null)
@@ -274,9 +277,13 @@ export const MutationEditorModal: FC<Props> = ({ apps, baseMutation, localMutati
274277

275278
const [editingMutation, setEditingMutation] = useState<MutationDto>(chooseEditingMutation())
276279
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
277-
278280
const [alert, setAlert] = useState<IAlert | null>(null)
279281

282+
// Reload the base mutation if it changed (e.g. if a mutation version was updated)
283+
useEffect(() => {
284+
setEditingMutation(chooseEditingMutation())
285+
}, [isLoading])
286+
280287
useEffect(() => {
281288
const doChecksForAlerts = (): IAlert | null => {
282289
if (!loggedInAccountId) return alerts.noWallet
@@ -378,7 +385,10 @@ export const MutationEditorModal: FC<Props> = ({ apps, baseMutation, localMutati
378385
/>
379386
</ImgWrapper>
380387
<TextWrapper>
381-
<p>{baseMutation.metadata.name}</p>
388+
<p>
389+
{baseMutation.metadata.name}{' '}
390+
<MutationVersionDropdown mutationId={baseMutation?.id ?? null} />
391+
</p>
382392
<span>
383393
by{' '}
384394
{!baseMutation.authorId && !loggedInAccountId
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useMutableWeb, useMutationVersions } from '@mweb/engine'
2+
import React from 'react'
3+
import { FC } from 'react'
4+
5+
const LatestKey = 'latest'
6+
7+
export const MutationVersionDropdown: FC<{ mutationId: string | null }> = ({ mutationId }) => {
8+
const {
9+
switchMutationVersion,
10+
selectedMutation,
11+
mutationVersions: currentMutationVersions,
12+
} = useMutableWeb()
13+
const { mutationVersions, areMutationVersionsLoading } = useMutationVersions(mutationId)
14+
15+
if (!mutationId) {
16+
return null
17+
}
18+
19+
if (!selectedMutation || areMutationVersionsLoading) {
20+
return <span>Loading...</span>
21+
}
22+
23+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
24+
if (mutationId) {
25+
switchMutationVersion(
26+
mutationId,
27+
e.target.value === LatestKey ? null : e.target.value?.toString()
28+
)
29+
}
30+
}
31+
32+
return (
33+
<select onChange={handleChange} value={currentMutationVersions[mutationId] ?? LatestKey}>
34+
{mutationVersions.map((version) => (
35+
<option key={version.version} value={version.version}>
36+
v{version.version}
37+
</option>
38+
))}
39+
<option value={LatestKey}>latest</option>
40+
</select>
41+
)
42+
}

libs/backend/src/services/application/application.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class ApplicationService {
2929
}
3030

3131
public async getApplication(appId: AppId): Promise<ApplicationDto | null> {
32-
const app = await this.applicationRepository.getItem(appId)
32+
const app = await this.applicationRepository.getItem({ id: appId })
3333
return app?.toDto() ?? null
3434
}
3535

libs/backend/src/services/base/base-agg.repository.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,24 @@ export class BaseAggRepository<T extends Base> implements IRepository<T> {
99
private local: IRepository<T>
1010
) {}
1111

12-
async getItem(id: EntityId, source?: EntitySourceType): Promise<T | null> {
12+
async getItem({
13+
id,
14+
source,
15+
version,
16+
}: {
17+
id: EntityId
18+
source?: EntitySourceType
19+
version?: string
20+
}): Promise<T | null> {
1321
if (source === EntitySourceType.Local) {
14-
return this.local.getItem(id)
22+
return this.local.getItem({ id, version })
1523
} else if (source === EntitySourceType.Origin) {
16-
return this.remote.getItem(id)
24+
return this.remote.getItem({ id, version })
1725
} else {
1826
// ToDo: why local is preferred?
19-
const localItem = await this.local.getItem(id)
27+
const localItem = await this.local.getItem({ id, version })
2028
if (localItem) return localItem
21-
return this.remote.getItem(id)
29+
return this.remote.getItem({ id, version })
2230
}
2331
}
2432

@@ -88,8 +96,52 @@ export class BaseAggRepository<T extends Base> implements IRepository<T> {
8896
}
8997
}
9098

99+
async getTagValue({
100+
id,
101+
source,
102+
tag,
103+
}: {
104+
id: EntityId
105+
source?: EntitySourceType
106+
tag: string
107+
}): Promise<string | null> {
108+
if (source === EntitySourceType.Local) {
109+
return this.local.getTagValue({ id, tag })
110+
} else if (source === EntitySourceType.Origin) {
111+
return this.remote.getTagValue({ id, tag })
112+
} else {
113+
throw new Error('Invalid source')
114+
}
115+
}
116+
117+
async getTags({ id, source }: { id: EntityId; source?: EntitySourceType }): Promise<string[]> {
118+
if (source === EntitySourceType.Local) {
119+
return this.local.getTags({ id })
120+
} else if (source === EntitySourceType.Origin) {
121+
return this.remote.getTags({ id })
122+
} else {
123+
throw new Error('Invalid source')
124+
}
125+
}
126+
127+
async getVersions({
128+
id,
129+
source,
130+
}: {
131+
id: EntityId
132+
source?: EntitySourceType
133+
}): Promise<string[]> {
134+
if (source === EntitySourceType.Local) {
135+
return this.local.getVersions({ id })
136+
} else if (source === EntitySourceType.Origin) {
137+
return this.remote.getVersions({ id })
138+
} else {
139+
throw new Error('Invalid source')
140+
}
141+
}
142+
91143
private async _deleteLocalItemIfExist(id: EntityId) {
92-
if (await this.local.getItem(id)) {
144+
if (await this.local.getItem({ id })) {
93145
await this.local.deleteItem(id)
94146
}
95147
}

libs/backend/src/services/base/base-local.repository.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
1717
this._entityKey = getEntity(EntityType).name
1818
}
1919

20-
async getItem(id: EntityId): Promise<T | null> {
20+
async getItem({ id, version }: { id: EntityId; version?: string }): Promise<T | null> {
2121
const parsedId = BaseLocalRepository._parseGlobalId(id)
2222
if (!parsedId) return null
2323

@@ -48,7 +48,7 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
4848
return true
4949
})
5050

51-
const items = await Promise.all(filteredKeys.map((id) => this.getItem(id)))
51+
const items = await Promise.all(filteredKeys.map((id) => this.getItem({ id })))
5252

5353
return items.filter((x) => x !== null)
5454
}
@@ -62,12 +62,12 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
6262
}
6363
return true
6464
})
65-
65+
6666
return filteredItems
6767
}
6868

6969
async createItem(item: T): Promise<T> {
70-
if (await this.getItem(item.id)) {
70+
if (await this.getItem({ id: item.id })) {
7171
throw new Error('Item with that ID already exists')
7272
}
7373

@@ -79,7 +79,7 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
7979
}
8080

8181
async editItem(item: T): Promise<T> {
82-
if (!(await this.getItem(item.id))) {
82+
if (!(await this.getItem({ id: item.id }))) {
8383
throw new Error('Item with that ID does not exist')
8484
}
8585

@@ -129,6 +129,18 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
129129
return entity
130130
}
131131

132+
async getVersions(options: { id: EntityId }): Promise<string[]> {
133+
throw new Error('Method not implemented.')
134+
}
135+
136+
async getTagValue(options: { id: EntityId; tag: string }): Promise<string | null> {
137+
throw new Error('Method not implemented.')
138+
}
139+
140+
async getTags(options: { id: EntityId }): Promise<string[]> {
141+
throw new Error('Method not implemented.')
142+
}
143+
132144
private static _parseGlobalId(globalId: EntityId): {
133145
authorId: string
134146
type: string

libs/backend/src/services/base/base.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export type BaseDto = {
77
authorId: string | null
88
blockNumber: number
99
timestamp: number
10+
version: string
1011
}

libs/backend/src/services/base/base.entity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class Base {
1515
source: EntitySourceType = EntitySourceType.Local
1616
blockNumber: number = 0 // ToDo: fake block number
1717
timestamp: number = 0 // ToDo: fake timestamp
18+
version: string = '0' // ToDo: fake version?
1819

1920
get authorId(): string | null {
2021
return this.id.split(KeyDelimiter)[0] ?? null
@@ -60,6 +61,7 @@ export class Base {
6061
source: this.source,
6162
blockNumber: this.blockNumber,
6263
timestamp: this.timestamp,
64+
version: this.version,
6365
}
6466
}
6567
}

0 commit comments

Comments
 (0)