implement repositories for media

This commit is contained in:
Mads Rasmussen
2023-02-07 19:16:06 +01:00
parent 9a5cb5a2c0
commit 24d4157b77
20 changed files with 795 additions and 275 deletions

View File

@@ -10,5 +10,5 @@ export interface MetaDashboardCollection {
pathname: string; pathname: string;
label?: string; label?: string;
entityType: string; entityType: string;
storeAlias: string; repositoryAlias: string;
} }

View File

@@ -21,8 +21,8 @@ import { UmbMediaTypeDetailStore } from './media/media-types/media-type.detail.s
import { UmbMediaTypeTreeStore } from './media/media-types/media-type.tree.store'; import { UmbMediaTypeTreeStore } from './media/media-types/media-type.tree.store';
import { UmbDocumentDetailStore } from './documents/documents/repository/document.detail.store'; import { UmbDocumentDetailStore } from './documents/documents/repository/document.detail.store';
import { UmbDocumentTreeStore } from './documents/documents/repository/document.tree.store'; import { UmbDocumentTreeStore } from './documents/documents/repository/document.tree.store';
import { UmbMediaDetailStore } from './media/media/media.detail.store'; import { UmbMediaDetailStore } from './media/media/repository/media.detail.store';
import { UmbMediaTreeStore } from './media/media/media.tree.store'; import { UmbMediaTreeStore } from './media/media/repository/media.tree.store';
import { UmbMemberTypeDetailStore } from './members/member-types/member-type.detail.store'; import { UmbMemberTypeDetailStore } from './members/member-types/member-type.detail.store';
import { UmbMemberTypeTreeStore } from './members/member-types/member-type.tree.store'; import { UmbMemberTypeTreeStore } from './members/member-types/member-type.tree.store';
import { UmbMemberGroupStore } from './members/member-groups/member-group.details.store'; import { UmbMemberGroupStore } from './members/member-groups/member-group.details.store';

View File

@@ -1,4 +1,3 @@
import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../repository/document.tree.store';
import { UmbDocumentRepository } from '../repository/document.repository'; import { UmbDocumentRepository } from '../repository/document.repository';
import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models';
@@ -9,8 +8,7 @@ const tree: ManifestTree = {
alias: treeAlias, alias: treeAlias,
name: 'Documents Tree', name: 'Documents Tree',
meta: { meta: {
storeAlias: UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN.toString(), repository: UmbDocumentRepository, // TODO: use alias instead of class
repository: UmbDocumentRepository,
}, },
}; };

View File

@@ -1,9 +1,11 @@
import { manifests as repositoryManifests } from './repository/manifests';
import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests'; import { manifests as sidebarMenuItemManifests } from './sidebar-menu-item/manifests';
import { manifests as treeManifests } from './tree/manifests'; import { manifests as treeManifests } from './tree/manifests';
import { manifests as workspaceManifests } from './workspace/manifests'; import { manifests as workspaceManifests } from './workspace/manifests';
import { manifests as entityActionsManifests } from './entity-actions/manifests'; import { manifests as entityActionsManifests } from './entity-actions/manifests';
export const manifests = [ export const manifests = [
...repositoryManifests,
...sidebarMenuItemManifests, ...sidebarMenuItemManifests,
...treeManifests, ...treeManifests,
...workspaceManifests, ...workspaceManifests,

View File

@@ -1,107 +0,0 @@
import type { MediaDetails } from '@umbraco-cms/models';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { ArrayState } from '@umbraco-cms/observable-api';
import { UmbStoreBase, UmbContentStore } from '@umbraco-cms/store';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
export const UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaDetailStore>('UmbMediaDetailStore');
/**
* @export
* @class UmbMediaDetailStore
* @extends {UmbStoreBase}
* @description - Data Store for Media
*/
export class UmbMediaDetailStore extends UmbStoreBase implements UmbContentStore<MediaDetails> {
#data = new ArrayState<MediaDetails>([], (x) => x.key);
constructor(host: UmbControllerHostInterface) {
super(host, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN.toString());
}
getByKey(key: string) {
// TODO: use backend cli when available.
fetch(`/umbraco/management/api/v1/media/details/${key}`)
.then((res) => res.json())
.then((data) => {
this.#data.append(data);
});
return this.#data.getObservablePart((documents) =>
documents.find((document) => document.key === key)
);
}
getScaffold(entityType: string, parentKey: string | null) {
return {
key: '',
name: '',
icon: '',
type: '',
hasChildren: false,
parentKey: '',
isTrashed: false,
properties: [
{
alias: '',
label: '',
description: '',
dataTypeKey: '',
},
],
data: [
{
alias: '',
value: '',
},
]
} as MediaDetails;
}
// TODO: make sure UI somehow can follow the status of this action.
save(data: MediaDetails[]) {
// fetch from server and update store
// TODO: use Fetcher API.
let body: string;
try {
body = JSON.stringify(data);
} catch (error) {
console.error(error);
return Promise.reject();
}
// TODO: use backend cli when available.
return fetch('/umbraco/management/api/v1/media/save', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((data: Array<MediaDetails>) => {
this.#data.append(data);
});
}
// TODO: how do we handle trashed items?
// TODO: How do we make trash available on details and tree store?
async trash(keys: Array<string>) {
// TODO: use backend cli when available.
const res = await fetch('/umbraco/management/api/v1/media/trash', {
method: 'POST',
body: JSON.stringify(keys),
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
this.#data.append(data);
}
}

View File

@@ -1,108 +0,0 @@
import type { Observable } from 'rxjs';
import { MediaResource, ContentTreeItem } from '@umbraco-cms/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { ArrayState } from '@umbraco-cms/observable-api';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/store';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTreeStore>('UmbMediaTreeStore');
// TODO: Stop using ContentTreeItem
export type MediaTreeItem = ContentTreeItem;
/**
* @export
* @class UmbMediaTreeStore
* @extends {UmbStoreBase}
* @description - Data Store for Media
*/
export class UmbMediaTreeStore extends UmbStoreBase implements UmbTreeStore<MediaTreeItem> {
#data = new ArrayState<MediaTreeItem>([], (x) => x.key);
constructor(host: UmbControllerHostInterface) {
super(host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString());
}
// TODO: how do we handle trashed items?
// TODO: How do we make trash available on details and tree store?
async trash(keys: Array<string>) {
// TODO: use backend cli when available.
const res = await fetch('/umbraco/management/api/v1/media/trash', {
method: 'POST',
body: JSON.stringify(keys),
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
this.#data.append(data);
}
async move(keys: Array<string>, destination: string) {
// TODO: use backend cli when available.
const res = await fetch('/umbraco/management/api/v1/media/move', {
method: 'POST',
body: JSON.stringify({ keys, destination }),
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
this.#data.append(data);
}
getTreeRoot(): Observable<Array<MediaTreeItem>> {
tryExecuteAndNotify(this._host, MediaResource.getTreeMediaRoot({})).then(({ data }) => {
if (data) {
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
this.#data.append(data.items);
}
});
// TODO: how do we handle trashed items?
// TODO: remove ignore when we know how to handle trashed items.
return this.#data.getObservablePart((items) =>
items.filter((item) => item.parentKey === null && !item.isTrashed)
);
}
getTreeItemChildren(key: string): Observable<Array<MediaTreeItem>> {
tryExecuteAndNotify(
this._host,
MediaResource.getTreeMediaChildren({
parentKey: key,
})
).then(({ data }) => {
if (data) {
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
this.#data.append(data.items);
}
});
// TODO: how do we handle trashed items?
// TODO: remove ignore when we know how to handle trashed items.
return this.#data.getObservablePart((items) =>
items.filter((item) => item.parentKey === key && !item.isTrashed)
);
}
getTreeItems(keys: Array<string>): Observable<Array<MediaTreeItem>> {
if (keys?.length > 0) {
tryExecuteAndNotify(
this._host,
MediaResource.getTreeMediaItem({
key: keys,
})
).then(({ data }) => {
if (data) {
// TODO: how do we handle if an item has been removed during this session(like in another tab or by another user)?
this.#data.append(data);
}
});
}
return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? '')));
}
}

View File

@@ -0,0 +1,13 @@
import { UmbMediaRepository } from './media.repository';
import { ManifestRepository } from 'libs/extensions-registry/repository.models';
export const DOCUMENT_REPOSITORY_ALIAS = 'Umb.Repository.Media';
const repository: ManifestRepository = {
type: 'repository',
alias: DOCUMENT_REPOSITORY_ALIAS,
name: 'Media Repository',
class: UmbMediaRepository,
};
export const manifests = [repository];

View File

@@ -0,0 +1,44 @@
import type { MediaDetails } from '@umbraco-cms/models';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { ArrayState } from '@umbraco-cms/observable-api';
import { UmbStoreBase } from '@umbraco-cms/store';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
/**
* @export
* @class UmbMediaDetailStore
* @extends {UmbStoreBase}
* @description - Data Store for Template Details
*/
export class UmbMediaDetailStore extends UmbStoreBase {
#data = new ArrayState<MediaDetails>([], (x) => x.key);
/**
* Creates an instance of UmbMediaDetailStore.
* @param {UmbControllerHostInterface} host
* @memberof UmbMediaDetailStore
*/
constructor(host: UmbControllerHostInterface) {
super(host, UmbMediaDetailStore.name);
}
/**
* Append a media to the store
* @param {MediaDetails} media
* @memberof UmbMediaDetailStore
*/
append(media: MediaDetails) {
this.#data.append([media]);
}
/**
* Removes media in the store with the given uniques
* @param {string[]} uniques
* @memberof UmbMediaDetailStore
*/
remove(uniques: string[]) {
this.#data.remove(uniques);
}
}
export const UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaDetailStore>(UmbMediaDetailStore.name);

View File

@@ -0,0 +1,227 @@
import type { RepositoryTreeDataSource } from '../../../../../libs/repository/repository-tree-data-source.interface';
import { MediaTreeServerDataSource } from './sources/media.tree.server.data';
import { UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media.tree.store';
import { UmbMediaDetailStore, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN } from './media.detail.store';
import { UmbMediaDetailServerDataSource } from './sources/media.detail.server.data';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { UmbContextConsumerController } from '@umbraco-cms/context-api';
import { ProblemDetails } from '@umbraco-cms/backend-api';
import type { UmbTreeRepository } from 'libs/repository/tree-repository.interface';
import { UmbDetailRepository } from '@umbraco-cms/repository';
import type { MediaDetails } from '@umbraco-cms/models';
import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification';
type ItemDetailType = MediaDetails;
// Move to documentation / JSdoc
/* We need to create a new instance of the repository from within the element context. We want the notifications to be displayed in the right context. */
// element -> context -> repository -> (store) -> data source
// All methods should be async and return a promise. Some methods might return an observable as part of the promise response.
export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepository<ItemDetailType> {
#init!: Promise<unknown>;
#host: UmbControllerHostInterface;
#treeSource: RepositoryTreeDataSource;
#treeStore?: UmbMediaTreeStore;
#detailDataSource: UmbMediaDetailServerDataSource;
#detailStore?: UmbMediaDetailStore;
#notificationService?: UmbNotificationService;
constructor(host: UmbControllerHostInterface) {
this.#host = host;
// TODO: figure out how spin up get the correct data source
this.#treeSource = new MediaTreeServerDataSource(this.#host);
this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host);
this.#init = Promise.all([
new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this.#treeStore = instance;
}),
new UmbContextConsumerController(this.#host, UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN, (instance) => {
this.#detailStore = instance;
}),
new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => {
this.#notificationService = instance;
}),
]);
}
async requestRootTreeItems() {
await this.#init;
const { data, error } = await this.#treeSource.getRootItems();
if (data) {
this.#treeStore?.appendItems(data.items);
}
return { data, error, asObservable: () => this.#treeStore!.rootItems };
}
async requestTreeItemsOf(parentKey: string | null) {
await this.#init;
if (!parentKey) {
const error: ProblemDetails = { title: 'Parent key is missing' };
return { data: undefined, error };
}
const { data, error } = await this.#treeSource.getChildrenOf(parentKey);
if (data) {
this.#treeStore?.appendItems(data.items);
}
return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentKey) };
}
async requestTreeItems(keys: Array<string>) {
await this.#init;
if (!keys) {
const error: ProblemDetails = { title: 'Keys are missing' };
return { data: undefined, error };
}
const { data, error } = await this.#treeSource.getItems(keys);
return { data, error, asObservable: () => this.#treeStore!.items(keys) };
}
async rootTreeItems() {
await this.#init;
return this.#treeStore!.rootItems;
}
async treeItemsOf(parentKey: string | null) {
await this.#init;
return this.#treeStore!.childrenOf(parentKey);
}
async treeItems(keys: Array<string>) {
await this.#init;
return this.#treeStore!.items(keys);
}
// DETAILS:
async createDetailsScaffold(parentKey: string | null) {
await this.#init;
if (!parentKey) {
throw new Error('Parent key is missing');
}
return this.#detailDataSource.createScaffold(parentKey);
}
async requestDetails(key: string) {
await this.#init;
// TODO: should we show a notification if the key is missing?
// Investigate what is best for Acceptance testing, cause in that perspective a thrown error might be the best choice?
if (!key) {
const error: ProblemDetails = { title: 'Key is missing' };
return { error };
}
const { data, error } = await this.#detailDataSource.get(key);
if (data) {
this.#detailStore?.append(data);
}
return { data, error };
}
// Could potentially be general methods:
async createDetail(template: ItemDetailType) {
await this.#init;
if (!template || !template.key) {
throw new Error('Template is missing');
}
const { error } = await this.#detailDataSource.insert(template);
if (!error) {
const notification = { data: { message: `Media created` } };
this.#notificationService?.peek('positive', notification);
}
// TODO: we currently don't use the detail store for anything.
// Consider to look up the data before fetching from the server
this.#detailStore?.append(template);
// TODO: Update tree store with the new item? or ask tree to request the new item?
return { error };
}
async saveDetail(document: ItemDetailType) {
await this.#init;
if (!document || !document.key) {
throw new Error('Template is missing');
}
const { error } = await this.#detailDataSource.update(document);
if (!error) {
const notification = { data: { message: `Document saved` } };
this.#notificationService?.peek('positive', notification);
}
// TODO: we currently don't use the detail store for anything.
// Consider to look up the data before fetching from the server
// Consider notify a workspace if a template is updated in the store while someone is editing it.
this.#detailStore?.append(document);
this.#treeStore?.updateItem(document.key, { name: document.name });
// TODO: would be nice to align the stores on methods/methodNames.
return { error };
}
// General:
async delete(key: string) {
await this.#init;
if (!key) {
throw new Error('Document key is missing');
}
const { error } = await this.#detailDataSource.delete(key);
if (!error) {
const notification = { data: { message: `Document deleted` } };
this.#notificationService?.peek('positive', notification);
}
// TODO: we currently don't use the detail store for anything.
// Consider to look up the data before fetching from the server.
// Consider notify a workspace if a template is deleted from the store while someone is editing it.
this.#detailStore?.remove([key]);
this.#treeStore?.removeItem(key);
// TODO: would be nice to align the stores on methods/methodNames.
return { error };
}
async move() {
alert('move');
}
async copy() {
alert('copy');
}
async sortChildrenOf() {
alert('sort');
}
}

View File

@@ -0,0 +1,91 @@
import { EntityTreeItem } from '@umbraco-cms/backend-api';
import { UmbContextToken } from '@umbraco-cms/context-api';
import { ArrayState } from '@umbraco-cms/observable-api';
import { UmbStoreBase } from '@umbraco-cms/store';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
/**
* @export
* @class UmbMediaTreeStore
* @extends {UmbStoreBase}
* @description - Tree Data Store for Templates
*/
// TODO: consider if tree store could be turned into a general EntityTreeStore class?
export class UmbMediaTreeStore extends UmbStoreBase {
#data = new ArrayState<EntityTreeItem>([], (x) => x.key);
/**
* Creates an instance of UmbMediaTreeStore.
* @param {UmbControllerHostInterface} host
* @memberof UmbMediaTreeStore
*/
constructor(host: UmbControllerHostInterface) {
super(host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString());
}
/**
* Appends items to the store
* @param {Array<EntityTreeItem>} items
* @memberof UmbMediaTreeStore
*/
appendItems(items: Array<EntityTreeItem>) {
this.#data.append(items);
}
/**
* Updates an item in the store
* @param {string} key
* @param {Partial<EntityTreeItem>} data
* @memberof UmbMediaTreeStore
*/
updateItem(key: string, data: Partial<EntityTreeItem>) {
const entries = this.#data.getValue();
const entry = entries.find((entry) => entry.key === key);
if (entry) {
this.#data.appendOne({ ...entry, ...data });
}
}
/**
* Removes an item from the store
* @param {string} key
* @memberof UmbMediaTreeStore
*/
removeItem(key: string) {
const entries = this.#data.getValue();
const entry = entries.find((entry) => entry.key === key);
if (entry) {
this.#data.remove([key]);
}
}
/**
* An observable to observe the root items
* @memberof UmbMediaTreeStore
*/
rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === null));
/**
* Returns an observable to observe the children of a given parent
* @param {(string | null)} parentKey
* @return {*}
* @memberof UmbMediaTreeStore
*/
childrenOf(parentKey: string | null) {
return this.#data.getObservablePart((items) => items.filter((item) => item.parentKey === parentKey));
}
/**
* Returns an observable to observe the items with the given keys
* @param {Array<string>} keys
* @return {*}
* @memberof UmbMediaTreeStore
*/
items(keys: Array<string>) {
return this.#data.getObservablePart((items) => items.filter((item) => keys.includes(item.key ?? '')));
}
}
export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTreeStore>(UmbMediaTreeStore.name);

View File

@@ -0,0 +1,202 @@
import { RepositoryDetailDataSource } from '@umbraco-cms/repository';
import { ProblemDetails } from '@umbraco-cms/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
import type { MediaDetails } from '@umbraco-cms/models';
/**
* A data source for the Template detail that fetches data from the server
* @export
* @class UmbTemplateDetailServerDataSource
* @implements {TemplateDetailDataSource}
*/
export class UmbMediaDetailServerDataSource implements RepositoryDetailDataSource<MediaDetails> {
#host: UmbControllerHostInterface;
/**
* Creates an instance of UmbMediaDetailServerDataSource.
* @param {UmbControllerHostInterface} host
* @memberof UmbMediaDetailServerDataSource
*/
constructor(host: UmbControllerHostInterface) {
this.#host = host;
}
/**
* Fetches a Media with the given key from the server
* @param {string} key
* @return {*}
* @memberof UmbMediaDetailServerDataSource
*/
async get(key: string) {
if (!key) {
const error: ProblemDetails = { title: 'Key is missing' };
return { error };
}
return tryExecuteAndNotify(
this.#host,
// TODO: use backend cli when available.
fetch(`/umbraco/management/api/v1/media/details/${key}`)
.then((res) => res.json())
.then((res) => res[0] || undefined)
);
}
/**
* Creates a new Media scaffold
* @param {(string | null)} parentKey
* @return {*}
* @memberof UmbMediaDetailServerDataSource
*/
async createScaffold(parentKey: string | null) {
const data: MediaDetails = {
key: '',
name: '',
icon: '',
type: '',
hasChildren: false,
parentKey: parentKey ?? '',
isTrashed: false,
properties: [
{
alias: '',
label: '',
description: '',
dataTypeKey: '',
},
],
data: [
{
alias: '',
value: '',
},
],
variants: [
{
name: '',
},
],
};
return { data };
}
/**
* Inserts a new Media on the server
* @param {Media} media
* @return {*}
* @memberof UmbMediaDetailServerDataSource
*/
async insert(media: MediaDetails) {
if (!media.key) {
//const error: ProblemDetails = { title: 'Media key is missing' };
return Promise.reject();
}
//const payload = { key: media.key, requestBody: media };
let body: string;
try {
body = JSON.stringify(media);
} catch (error) {
console.error(error);
return Promise.reject();
}
//return tryExecuteAndNotify(this.#host, MediaResource.postMedia(payload));
return tryExecuteAndNotify<MediaDetails>(
this.#host,
fetch('/umbraco/management/api/v1/media/save', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json',
},
}) as any
);
}
/**
* Updates a Media on the server
* @param {Media} Media
* @return {*}
* @memberof UmbMediaDetailServerDataSource
*/
// TODO: Error mistake in this:
async update(media: MediaDetails) {
if (!media.key) {
const error: ProblemDetails = { title: 'Media key is missing' };
return { error };
}
//const payload = { key: media.key, requestBody: media };
let body: string;
try {
body = JSON.stringify(media);
} catch (error) {
const myError: ProblemDetails = { title: 'JSON could not parse' };
return { error: myError };
}
return tryExecuteAndNotify<MediaDetails>(
this.#host,
fetch('/umbraco/management/api/v1/media/save', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json',
},
}) as any
);
}
/**
* Trash a Media on the server
* @param {Media} Media
* @return {*}
* @memberof UmbMediaDetailServerDataSource
*/
async trash(key: string) {
if (!key) {
const error: ProblemDetails = { title: 'Key is missing' };
return { error };
}
return tryExecuteAndNotify<MediaDetails>(
this.#host,
fetch('/umbraco/management/api/v1/media/trash', {
method: 'POST',
body: JSON.stringify([key]),
headers: {
'Content-Type': 'application/json',
},
}) as any
);
}
/**
* Deletes a Template on the server
* @param {string} key
* @return {*}
* @memberof UmbTemplateDetailServerDataSource
*/
// TODO: Error mistake in this:
async delete(key: string) {
if (!key) {
const error: ProblemDetails = { title: 'Key is missing' };
return { error };
}
return tryExecuteAndNotify(
this.#host,
fetch('/umbraco/management/api/v1/media/delete', {
method: 'POST',
body: JSON.stringify([key]),
headers: {
'Content-Type': 'application/json',
},
})
);
}
}

View File

@@ -0,0 +1,101 @@
import type { RepositoryTreeDataSource } from '../../../../../../libs/repository/repository-tree-data-source.interface';
import { ProblemDetails, MediaResource } from '@umbraco-cms/backend-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
/**
* A data source for the Media tree that fetches data from the server
* @export
* @class MediaTreeServerDataSource
* @implements {MediaTreeDataSource}
*/
export class MediaTreeServerDataSource implements RepositoryTreeDataSource {
#host: UmbControllerHostInterface;
// TODO: how do we handle trashed items?
async trashItems(keys: Array<string>) {
// TODO: use backend cli when available.
return tryExecuteAndNotify(
this.#host,
fetch('/umbraco/management/api/v1/media/trash', {
method: 'POST',
body: JSON.stringify(keys),
headers: {
'Content-Type': 'application/json',
},
})
);
}
async moveItems(keys: Array<string>, destination: string) {
// TODO: use backend cli when available.
return tryExecuteAndNotify(
this.#host,
fetch('/umbraco/management/api/v1/media/move', {
method: 'POST',
body: JSON.stringify({ keys, destination }),
headers: {
'Content-Type': 'application/json',
},
})
);
}
/**
* Creates an instance of MediaTreeServerDataSource.
* @param {UmbControllerHostInterface} host
* @memberof MediaTreeServerDataSource
*/
constructor(host: UmbControllerHostInterface) {
this.#host = host;
}
/**
* Fetches the root items for the tree from the server
* @return {*}
* @memberof MediaTreeServerDataSource
*/
async getRootItems() {
return tryExecuteAndNotify(this.#host, MediaResource.getTreeMediaRoot({}));
}
/**
* Fetches the children of a given parent key from the server
* @param {(string | null)} parentKey
* @return {*}
* @memberof MediaTreeServerDataSource
*/
async getChildrenOf(parentKey: string | null) {
if (!parentKey) {
const error: ProblemDetails = { title: 'Parent key is missing' };
return { error };
}
return tryExecuteAndNotify(
this.#host,
MediaResource.getTreeMediaChildren({
parentKey,
})
);
}
/**
* Fetches the items for the given keys from the server
* @param {Array<string>} keys
* @return {*}
* @memberof MediaTreeServerDataSource
*/
async getItems(keys: Array<string>) {
if (keys) {
const error: ProblemDetails = { title: 'Keys are missing' };
return { error };
}
return tryExecuteAndNotify(
this.#host,
MediaResource.getTreeMediaItem({
key: keys,
})
);
}
}

View File

@@ -1,4 +1,4 @@
import { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from '../media.tree.store'; import { UmbMediaRepository } from '../repository/media.repository';
import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models'; import type { ManifestTree, ManifestTreeItemAction } from '@umbraco-cms/models';
const treeAlias = 'Umb.Tree.Media'; const treeAlias = 'Umb.Tree.Media';
@@ -8,7 +8,7 @@ const tree: ManifestTree = {
alias: treeAlias, alias: treeAlias,
name: 'Media Tree', name: 'Media Tree',
meta: { meta: {
storeAlias: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), repository: UmbMediaRepository, // TODO: use alias instead of class
}, },
}; };

View File

@@ -1,4 +1,3 @@
import { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from '../media.tree.store';
import type { import type {
ManifestWorkspace, ManifestWorkspace,
ManifestWorkspaceAction, ManifestWorkspaceAction,
@@ -59,7 +58,7 @@ const workspaceViewCollections: Array<ManifestWorkspaceViewCollection> = [
pathname: 'collection', pathname: 'collection',
icon: 'umb:grid', icon: 'umb:grid',
entityType: 'media', entityType: 'media',
storeAlias: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), repositoryAlias: 'Umb.Repository.Media',
}, },
}, },
]; ];

View File

@@ -1,31 +1,87 @@
import { UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN } from "../media.detail.store"; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context';
import { UmbEntityWorkspaceManager } from "../../../shared/components/workspace/workspace-context/entity-manager-controller"; import { UmbMediaRepository } from '../repository/media.repository';
import { UmbWorkspaceContext } from "../../../shared/components/workspace/workspace-context/workspace-context"; import type { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface';
import { UmbWorkspaceEntityContextInterface } from "../../../shared/components/workspace/workspace-context/workspace-entity-context.interface"; import { appendToFrozenArray, ObjectState } from '@umbraco-cms/observable-api';
import type { MediaDetails } from "@umbraco-cms/models"; import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import type { MediaDetails } from '@umbraco-cms/models';
export class UmbWorkspaceMediaContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface<MediaDetails | undefined> { type EntityType = MediaDetails;
export class UmbMediaWorkspaceContext
extends UmbWorkspaceContext
implements UmbWorkspaceEntityContextInterface<EntityType | undefined>
{
#isNew = false;
#host: UmbControllerHostInterface;
#detailRepository: UmbMediaRepository;
#data = new ObjectState<EntityType | undefined>(undefined);
data = this.#data.asObservable();
name = this.#data.getObservablePart((data) => data?.name);
#manager = new UmbEntityWorkspaceManager(this._host, 'media', UMB_MEDIA_DETAIL_STORE_CONTEXT_TOKEN); constructor(host: UmbControllerHostInterface) {
super(host);
this.#host = host;
this.#detailRepository = new UmbMediaRepository(this.#host);
}
public readonly data = this.#manager.state.asObservable(); getData() {
public readonly name = this.#manager.state.getObservablePart((state) => state?.name); return this.#data.getValue();
}
getEntityKey() {
return this.getData()?.key || '';
}
getEntityType() {
return 'media';
}
setName(name: string) { setName(name: string) {
this.#manager.state.update({name: name}) this.#data.update({ name });
} }
getEntityType = this.#manager.getEntityType;
getUnique = this.#manager.getEntityKey;
getEntityKey = this.#manager.getEntityKey;
getStore = this.#manager.getStore;
getData = this.#manager.getData;
load = this.#manager.load;
create = this.#manager.create;
save = this.#manager.save;
destroy = this.#manager.destroy;
public setPropertyValue(alias: string, value: unknown) { setPropertyValue(alias: string, value: unknown) {
throw new Error('setPropertyValue is not implemented for UmbWorkspaceMediaContext'); const entry = { alias: alias, value: value };
const currentData = this.#data.value;
if (currentData) {
const newDataSet = appendToFrozenArray(currentData.data, entry, (x) => x.alias);
this.#data.update({ data: newDataSet });
}
}
async load(entityKey: string) {
const { data } = await this.#detailRepository.requestDetails(entityKey);
if (data) {
this.#isNew = false;
this.#data.next(data);
}
}
async createScaffold(parentKey: string | null) {
const { data } = await this.#detailRepository.createDetailsScaffold(parentKey);
if (!data) return;
this.#isNew = true;
this.#data.next(data);
}
async save() {
if (!this.#data.value) return;
if (this.#isNew) {
await this.#detailRepository.createDetail(this.#data.value);
} else {
await this.#detailRepository.saveDetail(this.#data.value);
}
// If it went well, then its not new anymore?.
this.#isNew = false;
}
async delete(key: string) {
await this.#detailRepository.delete(key);
}
public destroy(): void {
this.#data.complete();
} }
} }

View File

@@ -1,11 +1,12 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html } from 'lit'; import { css, html, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, state } from 'lit/decorators.js';
import { UmbWorkspaceMediaContext } from './media-workspace.context'; import type { UmbWorkspaceEntityElement } from '../../../shared/components/workspace/workspace-entity-element.interface';
import { UmbMediaWorkspaceContext } from './media-workspace.context';
import { UmbLitElement } from '@umbraco-cms/element'; import { UmbLitElement } from '@umbraco-cms/element';
@customElement('umb-media-workspace') @customElement('umb-document-workspace')
export class UmbMediaWorkspaceElement extends UmbLitElement { export class UmbMediaWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement {
static styles = [ static styles = [
UUITextStyles, UUITextStyles,
css` css`
@@ -17,19 +18,31 @@ export class UmbMediaWorkspaceElement extends UmbLitElement {
`, `,
]; ];
private _workspaceContext: UmbMediaWorkspaceContext = new UmbMediaWorkspaceContext(this);
private _workspaceContext: UmbWorkspaceMediaContext = new UmbWorkspaceMediaContext(this); @state()
_unique?: string;
public load(value: string) { public load(entityKey: string) {
this._workspaceContext?.load(value); this._workspaceContext.load(entityKey);
this._unique = entityKey;
} }
public create(parentKey: string | null) { public create(parentKey: string | null) {
this._workspaceContext?.create(parentKey); this._workspaceContext.createScaffold(parentKey);
} }
render() { render() {
return html`<umb-workspace-content alias="Umb.Workspace.Media"></umb-workspace-content>`; return html`<umb-workspace-content alias="Umb.Workspace.Media">
${this._unique
? html`
<umb-workspace-action-menu
slot="action-menu"
entity-type="media"
unique="${this._unique}"></umb-workspace-action-menu>
`
: nothing}
</umb-workspace-content>`;
} }
} }

View File

@@ -1,4 +1,3 @@
import { UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media/media.tree.store';
import type { ManifestDashboardCollection, ManifestSection } from '@umbraco-cms/models'; import type { ManifestDashboardCollection, ManifestSection } from '@umbraco-cms/models';
const sectionAlias = 'Umb.Section.Media'; const sectionAlias = 'Umb.Section.Media';
@@ -25,7 +24,7 @@ const dashboards: Array<ManifestDashboardCollection> = [
sections: [sectionAlias], sections: [sectionAlias],
pathname: 'media-management', pathname: 'media-management',
entityType: 'media', entityType: 'media',
storeAlias: UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN.toString(), repositoryAlias: 'Umb.Repository.Media',
}, },
}, },
]; ];

View File

@@ -156,16 +156,6 @@ export class UmbCollectionContext<
this.#selection.next(value); this.#selection.next(value);
} }
// TODO: Not all can trash, so maybe we need to differentiate on collection contexts or fix it with another architecture.
public trash(keys: string[]) {
this._store?.trash(keys);
}
// TODO: Not all can move, so maybe we need to differentiate on collection contexts or fix it with another architecture.
public move(keys: string[], destination: string) {
this._store?.move(keys, destination);
}
public clearSelection() { public clearSelection() {
this.#selection.next([]); this.#selection.next([]);
} }

View File

@@ -5,7 +5,7 @@ import { map } from 'rxjs';
import './collection-selection-actions.element'; import './collection-selection-actions.element';
import './collection-toolbar.element'; import './collection-toolbar.element';
import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from './collection.context'; import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from './collection.context';
import { createExtensionElement , umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import type { ManifestCollectionView, MediaDetails } from '@umbraco-cms/models'; import type { ManifestCollectionView, MediaDetails } from '@umbraco-cms/models';
import { UmbLitElement } from '@umbraco-cms/element'; import { UmbLitElement } from '@umbraco-cms/element';
import type { UmbObserverController } from '@umbraco-cms/observable-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api';
@@ -38,7 +38,7 @@ export class UmbCollectionElement extends UmbLitElement {
private _collectionContext?: UmbCollectionContext<MediaDetails>; private _collectionContext?: UmbCollectionContext<MediaDetails>;
private _entityType!: string; private _entityType!: string;
@property() @property({ type: String, attribute: 'entity-type' })
public get entityType(): string { public get entityType(): string {
return this._entityType; return this._entityType;
} }

View File

@@ -3,7 +3,6 @@ import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js'; import { customElement, state } from 'lit/decorators.js';
import '../collection.element'; import '../collection.element';
import { ifDefined } from 'lit-html/directives/if-defined.js'; import { ifDefined } from 'lit-html/directives/if-defined.js';
import { UmbMediaTreeStore } from '../../../media/media/media.tree.store';
import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from '../../../shared/collection/collection.context'; import { UmbCollectionContext, UMB_COLLECTION_CONTEXT_TOKEN } from '../../../shared/collection/collection.context';
import type { ManifestDashboardCollection } from '@umbraco-cms/models'; import type { ManifestDashboardCollection } from '@umbraco-cms/models';
import type { FolderTreeItem } from '@umbraco-cms/backend-api'; import type { FolderTreeItem } from '@umbraco-cms/backend-api';
@@ -25,7 +24,7 @@ export class UmbDashboardCollectionElement extends UmbLitElement {
]; ];
// TODO: Use the right type here: // TODO: Use the right type here:
private _collectionContext?: UmbCollectionContext<FolderTreeItem, UmbMediaTreeStore>; private _collectionContext?: UmbCollectionContext<FolderTreeItem, any>;
public manifest!: ManifestDashboardCollection; public manifest!: ManifestDashboardCollection;
@@ -37,14 +36,15 @@ export class UmbDashboardCollectionElement extends UmbLitElement {
if (!this._collectionContext) { if (!this._collectionContext) {
const manifestMeta = this.manifest.meta; const manifestMeta = this.manifest.meta;
this._entityType = manifestMeta.entityType as string; const repositoryAlias = manifestMeta.repositoryAlias;
this._collectionContext = new UmbCollectionContext(this, null, null, manifestMeta.storeAlias); this._entityType = manifestMeta.entityType;
this._collectionContext = new UmbCollectionContext(this, this._entityType, null, '', repositoryAlias);
this.provideContext(UMB_COLLECTION_CONTEXT_TOKEN, this._collectionContext); this.provideContext(UMB_COLLECTION_CONTEXT_TOKEN, this._collectionContext);
} }
} }
render() { render() {
return html`<umb-collection entityType=${ifDefined(this._entityType)}></umb-collection>`; return html`<umb-collection entity-type=${ifDefined(this._entityType)}></umb-collection>`;
} }
} }