diff --git a/src/assets/lang/da-dk.ts b/src/assets/lang/da-dk.ts index f226748e64..acd5f33aae 100644 --- a/src/assets/lang/da-dk.ts +++ b/src/assets/lang/da-dk.ts @@ -1822,6 +1822,9 @@ export default { permissionsDefault: 'Standardrettigheder', permissionsGranular: 'Granulære rettigheder', permissionsGranularHelp: 'Sæt rettigheder for specifikke noder', + permissionsEntityGroup_document: 'Indhold', + permissionsEntityGroup_media: 'Medie', + permissionsEntityGroup_member: 'Medlemmer', profile: 'Profil', searchAllChildren: "Søg alle 'børn'", languagesHelp: 'Tilføj sprog for at give brugerne adgang til at redigere', diff --git a/src/assets/lang/en-us.ts b/src/assets/lang/en-us.ts index a860981f33..37adbf86e2 100644 --- a/src/assets/lang/en-us.ts +++ b/src/assets/lang/en-us.ts @@ -1822,6 +1822,9 @@ export default { permissionsDefault: 'Default permissions', permissionsGranular: 'Granular permissions', permissionsGranularHelp: 'Set permissions for specific nodes', + permissionsEntityGroup_document: 'Content', + permissionsEntityGroup_media: 'Media', + permissionsEntityGroup_member: 'Member', profile: 'Profile', searchAllChildren: 'Search all children', languagesHelp: 'Limit the languages users have access to edit', diff --git a/src/mocks/browser-handlers.ts b/src/mocks/browser-handlers.ts index d6cc08b4c8..32e57dfdb8 100644 --- a/src/mocks/browser-handlers.ts +++ b/src/mocks/browser-handlers.ts @@ -8,7 +8,7 @@ import * as serverHandlers from './handlers/server.handlers.js'; import { handlers as upgradeHandlers } from './handlers/upgrade.handlers.js'; import { handlers as userHandlers } from './handlers/user.handlers.js'; import { handlers as telemetryHandlers } from './handlers/telemetry.handlers.js'; -import { handlers as userGroupsHandlers } from './handlers/user-group.handlers.js'; +import { handlers as userGroupsHandlers } from './handlers/user-group/index.js'; import { handlers as examineManagementHandlers } from './handlers/examine-management.handlers.js'; import { handlers as modelsBuilderHandlers } from './handlers/modelsbuilder.handlers.js'; import { handlers as healthCheckHandlers } from './handlers/health-check.handlers.js'; diff --git a/src/mocks/data/entity.data.ts b/src/mocks/data/entity.data.ts index 6be5d293fe..d6c442fe89 100644 --- a/src/mocks/data/entity.data.ts +++ b/src/mocks/data/entity.data.ts @@ -75,19 +75,10 @@ export class UmbEntityData extends UmbData { } delete(ids: Array) { - const deletedKeys = this.data - .filter((item) => { - if (!item.id) throw new Error('Item has no id'); - ids.includes(item.id); - }) - .map((item) => item.id); - this.data = this.data.filter((item) => { if (!item.id) throw new Error('Item has no id'); - ids.indexOf(item.id) === -1; + return !ids.includes(item.id); }); - - return deletedKeys; } updateData(updateItem: T) { diff --git a/src/mocks/data/media.data.ts b/src/mocks/data/media.data.ts index fd24e3d456..973dd02a2e 100644 --- a/src/mocks/data/media.data.ts +++ b/src/mocks/data/media.data.ts @@ -2,7 +2,11 @@ import type { MediaDetails } from '../../packages/media/media/index.js'; import { UmbEntityTreeData } from './entity-tree.data.js'; import { UmbEntityData } from './entity.data.js'; import { createContentTreeItem } from './utils.js'; -import { ContentTreeItemResponseModel, PagedMediaTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { + ContentTreeItemResponseModel, + MediaItemResponseModel, + PagedMediaTreeItemResponseModel, +} from '@umbraco-cms/backoffice/backend-api'; export const data: Array = [ { @@ -183,6 +187,14 @@ export const data: Array = [ }, ]; +const createMediaItem = (item: MediaDetails): MediaItemResponseModel => { + return { + id: item.id, + name: item.name, + icon: item.icon, + }; +}; + // Temp mocked database // TODO: all properties are optional in the server schema. I don't think this is correct. // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -194,6 +206,11 @@ class UmbMediaData extends UmbEntityData { super(data); } + getItems(ids: Array): Array { + const items = this.data.filter((item) => ids.includes(item.id ?? '')); + return items.map((item) => createMediaItem(item)); + } + getTreeRoot(): PagedMediaTreeItemResponseModel { const items = this.data.filter((item) => item.parentId === null); const treeItems = items.map((item) => createContentTreeItem(item)); diff --git a/src/mocks/data/user-group.data.ts b/src/mocks/data/user-group.data.ts new file mode 100644 index 0000000000..4fea6c2999 --- /dev/null +++ b/src/mocks/data/user-group.data.ts @@ -0,0 +1,78 @@ +import { UmbEntityData } from './entity.data.js'; +import { + UMB_USER_PERMISSION_DOCUMENT_CREATE, + UMB_USER_PERMISSION_DOCUMENT_DELETE, + UMB_USER_PERMISSION_DOCUMENT_READ, +} from '@umbraco-cms/backoffice/document'; +import { + PagedUserGroupResponseModel, + UserGroupItemResponseModel, + UserGroupResponseModel, +} from '@umbraco-cms/backoffice/backend-api'; + +const createUserGroupItem = (item: UserGroupResponseModel): UserGroupItemResponseModel => { + return { + name: item.name, + id: item.id, + icon: item.icon, + }; +}; + +// Temp mocked database +class UmbUserGroupData extends UmbEntityData { + constructor(data: Array) { + super(data); + } + + getAll(): PagedUserGroupResponseModel { + return { + total: this.data.length, + items: this.data, + }; + } + + getItems(ids: Array): Array { + const items = this.data.filter((item) => ids.includes(item.id ?? '')); + return items.map((item) => createUserGroupItem(item)); + } + + /** + * Returns a list of permissions for the given user group ids + * @param {string[]} userGroupIds + * @return {*} {string[]} + * @memberof UmbUserGroupData + */ + getPermissions(userGroupIds: string[]): string[] { + const permissions = this.data + .filter((userGroup) => userGroupIds.includes(userGroup.id || '')) + .map((userGroup) => (userGroup.permissions?.length ? userGroup.permissions : [])) + .flat(); + + // Remove duplicates + return [...new Set(permissions)]; + } +} + +export const data: Array = [ + { + id: 'c630d49e-4e7b-42ea-b2bc-edc0edacb6b1', + name: 'Administrators', + icon: 'umb:medal', + documentStartNodeId: 'all-property-editors-document-id', + permissions: [UMB_USER_PERMISSION_DOCUMENT_CREATE, UMB_USER_PERMISSION_DOCUMENT_DELETE], + }, + { + id: '9d24dc47-a4bf-427f-8a4a-b900f03b8a12', + name: 'User Group 1', + icon: 'umb:bell', + permissions: [UMB_USER_PERMISSION_DOCUMENT_DELETE], + }, + { + id: 'f4626511-b0d7-4ab1-aebc-a87871a5dcfa', + name: 'User Group 2', + icon: 'umb:ball', + permissions: [UMB_USER_PERMISSION_DOCUMENT_READ], + }, +]; + +export const umbUserGroupData = new UmbUserGroupData(data); diff --git a/src/mocks/data/user-groups.data.ts b/src/mocks/data/user-groups.data.ts deleted file mode 100644 index 968fb0919d..0000000000 --- a/src/mocks/data/user-groups.data.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { UmbEntityData } from './entity.data.js'; -import { PagedUserGroupResponseModel, UserGroupResponseModel } from '@umbraco-cms/backoffice/backend-api'; - -// Temp mocked database -class UmbUserGroupsData extends UmbEntityData { - constructor(data: Array) { - super(data); - } - - getAll(): PagedUserGroupResponseModel { - return { - total: this.data.length, - items: this.data, - }; - } -} - -export const data: Array = [ - { - id: 'c630d49e-4e7b-42ea-b2bc-edc0edacb6b1', - name: 'Administrators', - icon: 'umb:medal', - }, -]; - -export const umbUserGroupsData = new UmbUserGroupsData(data); diff --git a/src/mocks/data/users.data.ts b/src/mocks/data/user.data.ts similarity index 58% rename from src/mocks/data/users.data.ts rename to src/mocks/data/user.data.ts index c3ace50024..18dbc0ce37 100644 --- a/src/mocks/data/users.data.ts +++ b/src/mocks/data/user.data.ts @@ -1,9 +1,23 @@ -import { UmbData } from './data.js'; +import { UmbEntityData } from './entity.data.js'; +import { umbUserGroupData } from './user-group.data.js'; import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; -import { PagedUserResponseModel, UserResponseModel, UserStateModel } from '@umbraco-cms/backoffice/backend-api'; +import { + PagedUserResponseModel, + UpdateUserGroupsOnUserRequestModel, + UserItemResponseModel, + UserResponseModel, + UserStateModel, +} from '@umbraco-cms/backoffice/backend-api'; + +const createUserItem = (item: UserResponseModel): UserItemResponseModel => { + return { + name: item.name, + id: item.id, + }; +}; // Temp mocked database -class UmbUsersData extends UmbData { +class UmbUserData extends UmbEntityData { constructor(data: UserResponseModel[]) { super(data); } @@ -15,12 +29,21 @@ class UmbUsersData extends UmbData { }; } - getById(id: string): UserResponseModel | undefined { - return this.data.find((user) => user.id === id); + getItems(ids: Array): Array { + const items = this.data.filter((item) => ids.includes(item.id ?? '')); + return items.map((item) => createUserItem(item)); + } + + setUserGroups(data: UpdateUserGroupsOnUserRequestModel): void { + const users = this.data.filter((user) => data.userIds?.includes(user.id ?? '')); + users.forEach((user) => { + user.userGroupIds = data.userGroupIds; + }); } getCurrentUser(): UmbLoggedInUser { const firstUser = this.data[0]; + const permissions = firstUser.userGroupIds?.length ? umbUserGroupData.getPermissions(firstUser.userGroupIds) : []; return { id: firstUser.id, @@ -33,79 +56,9 @@ class UmbUsersData extends UmbData { languages: [], contentStartNodeIds: firstUser.contentStartNodeIds, mediaStartNodeIds: firstUser.mediaStartNodeIds, - permissions: [], + permissions, }; } - - save(id: string, saveItem: UserResponseModel) { - const foundIndex = this.data.findIndex((item) => item.id === id); - if (foundIndex !== -1) { - // update - this.data[foundIndex] = saveItem; - this.updateData(saveItem); - } else { - // new - this.data.push(saveItem); - } - - return saveItem; - } - - protected updateData(updateItem: UserResponseModel) { - const itemIndex = this.data.findIndex((item) => item.id === updateItem.id); - const item = this.data[itemIndex]; - - console.log('updateData', updateItem, itemIndex, item); - - if (!item) return; - - const itemKeys = Object.keys(item); - const newItem = {}; - - for (const [key] of Object.entries(updateItem)) { - if (itemKeys.indexOf(key) !== -1) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - newItem[key] = updateItem[key]; - } - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.data[itemIndex] = newItem; - - console.log('updateData', this.data[itemIndex]); - } - - // updateUserGroup(ids: string[], userGroup: string) { - // this.data.forEach((user) => { - // if (ids.includes(user.id)) { - // } else { - // } - - // this.updateData(user); - // }); - - // return this.data.map((user) => user.id); - // } - - // enable(ids: string[]) { - // const users = this.data.filter((user) => ids.includes(user.id)); - // users.forEach((user) => { - // user.status = 'enabled'; - // this.updateData(user); - // }); - // return users.map((user) => user.id); - // } - - // disable(ids: string[]) { - // const users = this.data.filter((user) => ids.includes(user.id)); - // users.forEach((user) => { - // user.status = 'disabled'; - // this.updateData(user); - // }); - // return users.map((user) => user.id); - // } } export const data: Array = [ @@ -124,7 +77,11 @@ export const data: Array = [ updateDate: '2/10/2022', createDate: '3/13/2022', failedLoginAttempts: 946, - userGroupIds: ['c630d49e-4e7b-42ea-b2bc-edc0edacb6b1'], + userGroupIds: [ + 'c630d49e-4e7b-42ea-b2bc-edc0edacb6b1', + '9d24dc47-a4bf-427f-8a4a-b900f03b8a12', + 'f4626511-b0d7-4ab1-aebc-a87871a5dcfa', + ], }, { id: '82e11d3d-b91d-43c9-9071-34d28e62e81d', @@ -196,4 +153,4 @@ export const data: Array = [ }, ]; -export const umbUsersData = new UmbUsersData(data); +export const umbUsersData = new UmbUserData(data); diff --git a/src/mocks/handlers/dictionary.handlers.ts b/src/mocks/handlers/dictionary.handlers.ts index 874bb40a16..69da232146 100644 --- a/src/mocks/handlers/dictionary.handlers.ts +++ b/src/mocks/handlers/dictionary.handlers.ts @@ -151,9 +151,9 @@ export const handlers = [ const id = req.params.id as string; if (!id) return; - const deletedKeys = umbDictionaryData.delete([id]); + umbDictionaryData.delete([id]); - return res(ctx.status(200), ctx.json(deletedKeys)); + return res(ctx.status(200)); }), // TODO => handle properly, querystring breaks handler @@ -165,7 +165,7 @@ export const handlers = [ const item = umbDictionaryData.getById(id); alert( - `Downloads file for dictionary "${item?.name}", ${includeChildren === 'true' ? 'with' : 'without'} children.` + `Downloads file for dictionary "${item?.name}", ${includeChildren === 'true' ? 'with' : 'without'} children.`, ); return res(ctx.status(200)); }), diff --git a/src/mocks/handlers/media.handlers.ts b/src/mocks/handlers/media.handlers.ts index 4b15c2d898..6ec9a61774 100644 --- a/src/mocks/handlers/media.handlers.ts +++ b/src/mocks/handlers/media.handlers.ts @@ -1,8 +1,16 @@ const { rest } = window.MockServiceWorker; import { umbMediaData } from '../data/media.data.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; // TODO: add schema export const handlers = [ + rest.get(umbracoPath('/media/item'), (req, res, ctx) => { + const ids = req.url.searchParams.getAll('id'); + if (!ids) return; + const items = umbMediaData.getItems(ids); + return res(ctx.status(200), ctx.json(items)); + }), + rest.get('/umbraco/management/api/v1/media/details/:id', (req, res, ctx) => { console.warn('Please move to schema'); const id = req.params.id as string; diff --git a/src/mocks/handlers/partial-views.handlers.ts b/src/mocks/handlers/partial-views.handlers.ts index b51faa885a..12112a7b34 100644 --- a/src/mocks/handlers/partial-views.handlers.ts +++ b/src/mocks/handlers/partial-views.handlers.ts @@ -45,16 +45,17 @@ const detailHandlers: RestHandler>[] = [ rest.delete(umbracoPath('/partial-view'), (req, res, ctx) => { const path = req.url.searchParams.get('path'); if (!path) return res(ctx.status(400)); - const response = umbPartialViewsData.delete([path]); - return res(ctx.status(200), ctx.json(response)); + umbPartialViewsData.delete([path]); + return res(ctx.status(200)); }), + rest.put(umbracoPath('/partial-view'), (req, res, ctx) => { const requestBody = req.json() as CreateTextFileViewModelBaseModel; if (!requestBody) return res(ctx.status(400, 'no body found')); - const response = umbPartialViewsData.updateData(requestBody); + umbPartialViewsData.updateData(requestBody); return res(ctx.status(200)); }), ]; const folderHandlers: RestHandler>[] = []; -export const handlers = [...treeHandlers, ...detailHandlers, ...folderHandlers] \ No newline at end of file +export const handlers = [...treeHandlers, ...detailHandlers, ...folderHandlers]; diff --git a/src/mocks/handlers/user-group.handlers.ts b/src/mocks/handlers/user-group.handlers.ts deleted file mode 100644 index 571943d163..0000000000 --- a/src/mocks/handlers/user-group.handlers.ts +++ /dev/null @@ -1,21 +0,0 @@ -const { rest } = window.MockServiceWorker; -import { umbUserGroupsData } from '../data/user-groups.data.js'; -import { umbracoPath } from '@umbraco-cms/backoffice/utils'; - -const slug = '/user-group'; - -export const handlers = [ - rest.get(umbracoPath(`${slug}`), (req, res, ctx) => { - const response = umbUserGroupsData.getAll(); - - return res(ctx.status(200), ctx.json(response)); - }), - - rest.get(umbracoPath(`${slug}/:id`), (req, res, ctx) => { - const id = req.params.id as string; - if (!id) return; - const userGroup = umbUserGroupsData.getById(id); - - return res(ctx.status(200), ctx.json(userGroup)); - }), -]; diff --git a/src/mocks/handlers/user-group/collection.handlers.ts b/src/mocks/handlers/user-group/collection.handlers.ts new file mode 100644 index 0000000000..56ffe6f449 --- /dev/null +++ b/src/mocks/handlers/user-group/collection.handlers.ts @@ -0,0 +1,11 @@ +const { rest } = window.MockServiceWorker; +import { umbUserGroupData } from '../../data/user-group.data.js'; +import { slug } from './slug.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const collectionHandlers = [ + rest.get(umbracoPath(`${slug}`), (req, res, ctx) => { + const response = umbUserGroupData.getAll(); + return res(ctx.status(200), ctx.json(response)); + }), +]; diff --git a/src/mocks/handlers/user-group/detail.handlers.ts b/src/mocks/handlers/user-group/detail.handlers.ts new file mode 100644 index 0000000000..100493e1c8 --- /dev/null +++ b/src/mocks/handlers/user-group/detail.handlers.ts @@ -0,0 +1,44 @@ +const { rest } = window.MockServiceWorker; +import { umbUserGroupData } from '../../data/user-group.data.js'; +import { slug } from './slug.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const detailHandlers = [ + rest.post(umbracoPath(`${slug}`), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + umbUserGroupData.insert(data); + + return res(ctx.status(200)); + }), + + rest.get(umbracoPath(`${slug}/:id`), (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return; + + const dataType = umbUserGroupData.getById(id); + + return res(ctx.status(200), ctx.json(dataType)); + }), + + rest.put(umbracoPath(`${slug}/:id`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return; + const data = await req.json(); + if (!data) return; + + umbUserGroupData.save(id, data); + + return res(ctx.status(200)); + }), + + rest.delete(umbracoPath(`${slug}/:id`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return; + + umbUserGroupData.delete([id]); + + return res(ctx.status(200)); + }), +]; diff --git a/src/mocks/handlers/user-group/index.ts b/src/mocks/handlers/user-group/index.ts new file mode 100644 index 0000000000..c49236b99a --- /dev/null +++ b/src/mocks/handlers/user-group/index.ts @@ -0,0 +1,5 @@ +import { collectionHandlers } from './collection.handlers.js'; +import { detailHandlers } from './detail.handlers.js'; +import { itemHandlers } from './item.handlers.js'; + +export const handlers = [...itemHandlers, ...collectionHandlers, ...detailHandlers]; diff --git a/src/mocks/handlers/user-group/item.handlers.ts b/src/mocks/handlers/user-group/item.handlers.ts new file mode 100644 index 0000000000..c0d7a0e52d --- /dev/null +++ b/src/mocks/handlers/user-group/item.handlers.ts @@ -0,0 +1,14 @@ +const { rest } = window.MockServiceWorker; +import { umbUserGroupData } from '../../data/user-group.data.js'; +import { slug } from './slug.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const itemHandlers = [ + rest.get(umbracoPath(`${slug}/item`), (req, res, ctx) => { + const ids = req.url.searchParams.getAll('id'); + if (!ids) return; + const items = umbUserGroupData.getItems(ids); + + return res(ctx.status(200), ctx.json(items)); + }), +]; diff --git a/src/mocks/handlers/user-group/slug.ts b/src/mocks/handlers/user-group/slug.ts new file mode 100644 index 0000000000..25f6d8e6d1 --- /dev/null +++ b/src/mocks/handlers/user-group/slug.ts @@ -0,0 +1 @@ +export const slug = '/user-group'; diff --git a/src/mocks/handlers/user.handlers.ts b/src/mocks/handlers/user.handlers.ts index 9da37812be..175f439d69 100644 --- a/src/mocks/handlers/user.handlers.ts +++ b/src/mocks/handlers/user.handlers.ts @@ -1,11 +1,28 @@ const { rest } = window.MockServiceWorker; -import { umbUsersData } from '../data/users.data.js'; +import { umbUsersData } from '../data/user.data.js'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; const slug = '/user'; export const handlers = [ + rest.get(umbracoPath(`${slug}/item`), (req, res, ctx) => { + const ids = req.url.searchParams.getAll('id'); + if (!ids) return; + const items = umbUsersData.getItems(ids); + + return res(ctx.status(200), ctx.json(items)); + }), + + rest.post(umbracoPath(`${slug}/set-user-groups`), async (req, res, ctx) => { + const data = await req.json(); + if (!data) return; + + umbUsersData.setUserGroups(data); + + return res(ctx.status(200)); + }), + rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => { //TODO: Implementer filter const response = umbUsersData.getAll(); @@ -23,7 +40,7 @@ export const handlers = [ ctx.status(200), ctx.json({ sections: ['Umb.Section.Content', 'Umb.Section.Media', 'Umb.Section.Settings', 'My.Section.Custom'], - }) + }), ); }), diff --git a/src/packages/core/components/input-section/input-section.element.ts b/src/packages/core/components/input-section/input-section.element.ts index a93649fa3e..73427c9aca 100644 --- a/src/packages/core/components/input-section/input-section.element.ts +++ b/src/packages/core/components/input-section/input-section.element.ts @@ -1,11 +1,11 @@ import { UmbInputListBaseElement } from '../input-list-base/input-list-base.js'; -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UMB_SECTION_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; import { ManifestSection, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @customElement('umb-input-section') -export class UmbInputPickerSectionElement extends UmbInputListBaseElement { +export class UmbInputSectionElement extends UmbInputListBaseElement { @state() private _sections: Array = []; @@ -47,7 +47,7 @@ export class UmbInputPickerSectionElement extends UmbInputListBaseElement { label="remove" color="danger"> - ` + `, )} `; @@ -85,6 +85,6 @@ export class UmbInputPickerSectionElement extends UmbInputListBaseElement { declare global { interface HTMLElementTagNameMap { - 'umb-input-section': UmbInputPickerSectionElement; + 'umb-input-section': UmbInputSectionElement; } } diff --git a/src/packages/core/components/input-section/input-section.stories.ts b/src/packages/core/components/input-section/input-section.stories.ts index 30367b89ae..b39e4966d8 100644 --- a/src/packages/core/components/input-section/input-section.stories.ts +++ b/src/packages/core/components/input-section/input-section.stories.ts @@ -1,8 +1,8 @@ import { Meta, StoryObj } from '@storybook/web-components'; import './input-section.element.js'; -import type { UmbInputPickerSectionElement } from './input-section.element.js'; +import type { UmbInputSectionElement } from './input-section.element.js'; -const meta: Meta = { +const meta: Meta = { title: 'Components/Inputs/Section', component: 'umb-input-section', argTypes: { @@ -22,7 +22,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Overview: Story = { args: { diff --git a/src/packages/core/extension-registry/conditions/types.ts b/src/packages/core/extension-registry/conditions/types.ts index 2a6e1f5d74..943dbe8d41 100644 --- a/src/packages/core/extension-registry/conditions/types.ts +++ b/src/packages/core/extension-registry/conditions/types.ts @@ -1,10 +1,14 @@ import type { SectionAliasConditionConfig } from './section-alias.condition.js'; import type { SwitchConditionConfig } from './switch.condition.js'; import type { WorkspaceAliasConditionConfig } from '@umbraco-cms/backoffice/workspace'; -import { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; +import type { UserPermissionConditionConfig } from '@umbraco-cms/backoffice/current-user'; +/* TODO: in theory should't the core package import from other packages. +Are there any other way we can do this? */ export type ConditionTypes = | SectionAliasConditionConfig | WorkspaceAliasConditionConfig | SwitchConditionConfig + | UserPermissionConditionConfig | UmbConditionConfigBase; diff --git a/src/packages/core/extension-registry/models/index.ts b/src/packages/core/extension-registry/models/index.ts index ff6dcfd29c..65ba8fdb27 100644 --- a/src/packages/core/extension-registry/models/index.ts +++ b/src/packages/core/extension-registry/models/index.ts @@ -28,6 +28,8 @@ import type { ManifestWorkspace } from './workspace.model.js'; import type { ManifestWorkspaceAction } from './workspace-action.model.js'; import type { ManifestWorkspaceEditorView } from './workspace-editor-view.model.js'; import type { ManifestWorkspaceViewCollection } from './workspace-view-collection.model.js'; +import type { ManifestUserPermission } from './user-permission.model.js'; +import type { ManifestUserGranularPermission } from './user-granular-permission.model.js'; import type { ManifestBase, ManifestBundle, @@ -65,6 +67,8 @@ export * from './workspace-action.model.js'; export * from './workspace-view-collection.model.js'; export * from './workspace-editor-view.model.js'; export * from './workspace.model.js'; +export * from './user-permission.model.js'; +export * from './user-granular-permission.model.js'; export type ManifestTypes = | ManifestBundle @@ -106,4 +110,6 @@ export type ManifestTypes = | ManifestWorkspaceAction | ManifestWorkspaceEditorView | ManifestWorkspaceViewCollection + | ManifestUserPermission + | ManifestUserGranularPermission | ManifestBase; diff --git a/src/packages/core/extension-registry/models/user-granular-permission.model.ts b/src/packages/core/extension-registry/models/user-granular-permission.model.ts new file mode 100644 index 0000000000..fc6698a802 --- /dev/null +++ b/src/packages/core/extension-registry/models/user-granular-permission.model.ts @@ -0,0 +1,10 @@ +import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestUserGranularPermission extends ManifestElement { + type: 'userGranularPermission'; + meta: MetaUserGranularPermission; +} + +export interface MetaUserGranularPermission { + entityType: string; +} diff --git a/src/packages/core/extension-registry/models/user-permission.model.ts b/src/packages/core/extension-registry/models/user-permission.model.ts new file mode 100644 index 0000000000..19825ab970 --- /dev/null +++ b/src/packages/core/extension-registry/models/user-permission.model.ts @@ -0,0 +1,13 @@ +import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestUserPermission extends ManifestBase { + type: 'userPermission'; + meta: MetaUserPermission; +} + +export interface MetaUserPermission { + label: string; + entityType: string; + description?: string; + group?: string; +} diff --git a/src/packages/core/modal/common/entity-user-permission-settings/entity-user-permission-settings-modal.element.ts b/src/packages/core/modal/common/entity-user-permission-settings/entity-user-permission-settings-modal.element.ts new file mode 100644 index 0000000000..cacdb22527 --- /dev/null +++ b/src/packages/core/modal/common/entity-user-permission-settings/entity-user-permission-settings-modal.element.ts @@ -0,0 +1,112 @@ +import { html, customElement, property, state, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { + UmbEntityUserPermissionSettingsModalData, + UmbEntityUserPermissionSettingsModalResult, + UmbModalContext, +} from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { ManifestUserPermission, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; + +@customElement('umb-entity-user-permission-settings-modal') +export class UmbEntityUserPermissionSettingsModalElement extends UmbLitElement { + @property({ attribute: false }) + modalContext?: UmbModalContext; + + @property({ type: Object }) + data?: UmbEntityUserPermissionSettingsModalData; + + @state() + private _userPermissionManifests: Array = []; + + private _handleConfirm() { + this.modalContext?.submit(); + } + + private _handleCancel() { + this.modalContext?.reject(); + } + + constructor() { + super(); + this.observe( + umbExtensionsRegistry.extensionsOfType('userPermission'), + (userPermissionManifests) => (this._userPermissionManifests = userPermissionManifests), + ); + } + + render() { + return html` + + + Render user permissions for ${this.data?.entityType} ${this.data?.unique} + ${this._userPermissionManifests.map((permission) => this.#renderPermission(permission))} + + Cancel + + + + `; + } + + #onChangeUserPermission(event: UUIBooleanInputEvent, userPermissionManifest: ManifestUserPermission) { + console.log(userPermissionManifest); + console.log(event.target.checked); + } + + #isAllowed(userPermissionManifest: ManifestUserPermission) { + return true; + //return this._userGroup?.permissions?.includes(userPermissionManifest.alias); + } + + #renderPermission(userPermissionManifest: ManifestUserPermission) { + return html`
+ this.#onChangeUserPermission(event, userPermissionManifest)}> +
+
${userPermissionManifest.meta.label}
+ ${userPermissionManifest.meta.description} +
+
+
`; + } + + static styles = [ + UmbTextStyles, + css` + .permission-toggle { + display: flex; + align-items: center; + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-space-3) 0 var(--uui-size-space-4) 0; + } + + .permission-meta { + margin-left: var(--uui-size-space-4); + line-height: 1.2em; + } + + .permission-name { + font-weight: bold; + } + `, + ]; +} + +export default UmbEntityUserPermissionSettingsModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-user-permission-modal': UmbEntityUserPermissionSettingsModalElement; + } +} diff --git a/src/packages/core/modal/common/manifests.ts b/src/packages/core/modal/common/manifests.ts index 6d7044fd52..9f8b55d4f9 100644 --- a/src/packages/core/modal/common/manifests.ts +++ b/src/packages/core/modal/common/manifests.ts @@ -61,6 +61,12 @@ const modals: Array = [ name: 'Tree Picker Modal', loader: () => import('./tree-picker/tree-picker-modal.element.js'), }, + { + type: 'modal', + alias: 'Umb.Modal.EntityUserPermissionSettings', + name: 'Entity User Permission Settings Modal', + loader: () => import('./entity-user-permission-settings/entity-user-permission-settings-modal.element.js'), + }, ]; export const manifests = [...modals]; diff --git a/src/packages/core/modal/common/tree-picker/tree-picker-modal.element.ts b/src/packages/core/modal/common/tree-picker/tree-picker-modal.element.ts index e0199b2663..3ecae28a7d 100644 --- a/src/packages/core/modal/common/tree-picker/tree-picker-modal.element.ts +++ b/src/packages/core/modal/common/tree-picker/tree-picker-modal.element.ts @@ -1,9 +1,10 @@ import type { UmbTreeElement } from '../../../tree/tree.element.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; +import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbTreePickerModalData, UmbPickerModalResult } from '@umbraco-cms/backoffice/modal'; import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; import { TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbSelectedEvent } from '@umbraco-cms/backoffice/events'; @customElement('umb-tree-picker-modal') export class UmbTreePickerModalElement extends UmbModalBaseElement< @@ -27,6 +28,7 @@ export class UmbTreePickerModalElement; +} + +export type UmbEntityUserPermissionSettingsModalResult = undefined; + +export const UMB_ENTITY_USER_PERMISSION_MODAL = new UmbModalToken< + UmbEntityUserPermissionSettingsModalData, + UmbEntityUserPermissionSettingsModalResult +>('Umb.Modal.EntityUserPermissionSettings', { + type: 'sidebar', +}); diff --git a/src/packages/core/modal/token/index.ts b/src/packages/core/modal/token/index.ts index 63163c3cb8..6871d578f7 100644 --- a/src/packages/core/modal/token/index.ts +++ b/src/packages/core/modal/token/index.ts @@ -33,3 +33,4 @@ export * from './data-type-picker-modal.token.js'; export * from './workspace-modal.token.js'; export * from './data-type-picker-flow-modal.token.js'; export * from './data-type-picker-flow-data-type-picker-modal.token.js'; +export * from './entity-user-permission-settings-modal.token.js'; diff --git a/src/packages/core/picker-input/picker-input.context.ts b/src/packages/core/picker-input/picker-input.context.ts index b10586a5de..a77b907fee 100644 --- a/src/packages/core/picker-input/picker-input.context.ts +++ b/src/packages/core/picker-input/picker-input.context.ts @@ -37,7 +37,7 @@ export class UmbPickerInputContext host: UmbControllerHostElement, repositoryAlias: string, modalAlias: string | UmbModalToken, - getUniqueMethod?: (entry: ItemType) => string | undefined + getUniqueMethod?: (entry: ItemType) => string | undefined, ) { this.host = host; this.modalAlias = modalAlias; @@ -83,9 +83,6 @@ export class UmbPickerInputContext } async requestRemoveItem(unique: string) { - await this.#init; - if (!this.repository) throw new Error('Repository is not initialized'); - // TODO: id won't always be available on the model, so we need to get the unique property from somewhere. Maybe the repository? const item = this.#itemManager.getItems().find((item) => this.#getUnique(item) === unique); if (!item) throw new Error('Could not find item with unique: ' + unique); @@ -104,5 +101,6 @@ export class UmbPickerInputContext #removeItem(unique: string) { const newSelection = this.getSelection().filter((value) => value !== unique); this.setSelection(newSelection); + this.host.dispatchEvent(new UmbChangeEvent()); } } diff --git a/src/packages/core/tree/tree.context.ts b/src/packages/core/tree/tree.context.ts index ef94d2f024..1e9c5fe3a6 100644 --- a/src/packages/core/tree/tree.context.ts +++ b/src/packages/core/tree/tree.context.ts @@ -1,12 +1,12 @@ -import { Observable, map } from '@umbraco-cms/backoffice/external/rxjs'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbPagedData, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; import { ManifestTree, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import { createExtensionClass } from '@umbraco-cms/backoffice/extension-api'; import { ProblemDetails, TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; -import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; import { UmbSelectionManagerBase } from '@umbraco-cms/backoffice/utils'; +import { UmbSelectedEvent } from '@umbraco-cms/backoffice/events'; // TODO: update interface export interface UmbTreeContext extends UmbBaseController { @@ -102,12 +102,12 @@ export class UmbTreeContextBase public select(unique: string | null) { if (!this.getSelectable()) return; this.#selectionManager.select(unique); - this._host.getHostElement().dispatchEvent(new CustomEvent('selected')); + this._host.getHostElement().dispatchEvent(new UmbSelectedEvent()); } public deselect(unique: string | null) { this.#selectionManager.deselect(unique); - this._host.getHostElement().dispatchEvent(new CustomEvent('selected')); + this._host.getHostElement().dispatchEvent(new UmbSelectedEvent()); } public async requestTreeRoot() { @@ -144,7 +144,7 @@ export class UmbTreeContextBase if (!treeManifest) return; this.#observeRepository(treeManifest); }, - '_observeTreeManifest' + '_observeTreeManifest', ); } } @@ -166,7 +166,7 @@ export class UmbTreeContextBase throw new Error('Could not create repository with alias: ' + repositoryAlias + ''); } }, - '_observeRepository' + '_observeRepository', ); } } diff --git a/src/packages/core/tree/tree.element.ts b/src/packages/core/tree/tree.element.ts index de7c270b54..aa06b67efa 100644 --- a/src/packages/core/tree/tree.element.ts +++ b/src/packages/core/tree/tree.element.ts @@ -119,7 +119,7 @@ export class UmbTreeElement extends UmbLitElement { this._items, // TODO: use unique here: (item, index) => item.name + '___' + index, - (item) => html`` + (item) => html``, )} `; } diff --git a/src/packages/documents/documents/components/index.ts b/src/packages/documents/documents/components/index.ts index 339d030d1d..a644a4c547 100644 --- a/src/packages/documents/documents/components/index.ts +++ b/src/packages/documents/documents/components/index.ts @@ -1 +1,2 @@ export * from './input-document/input-document.element.js'; +export * from './input-document-granular-permission/input-document-granular-permission.element.js'; diff --git a/src/packages/documents/documents/components/input-document-granular-permission/input-document-granular-permission.element.ts b/src/packages/documents/documents/components/input-document-granular-permission/input-document-granular-permission.element.ts new file mode 100644 index 0000000000..1ab33f7290 --- /dev/null +++ b/src/packages/documents/documents/components/input-document-granular-permission/input-document-granular-permission.element.ts @@ -0,0 +1,122 @@ +import { css, html, customElement, property, state, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { + UmbModalManagerContext, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, + UMB_CONFIRM_MODAL, + UMB_DOCUMENT_PICKER_MODAL, +} from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbDocumentRepository } from '@umbraco-cms/backoffice/document'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/events'; +import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('umb-input-document-granular-permission') +export class UmbInputDocumentGranularPermissionElement extends FormControlMixin(UmbLitElement) { + private _selectedIds: Array = []; + public get selectedIds(): Array { + return this._selectedIds; + } + public set selectedIds(ids: Array) { + this._selectedIds = ids; + super.value = ids.join(','); + this.#observePickedDocuments(); + } + + @property() + public set value(idsString: string) { + if (idsString !== this._value) { + this.selectedIds = idsString.split(/[ ,]+/); + } + } + + @state() + private _items?: Array; + + #documentRepository = new UmbDocumentRepository(this); + #modalContext?: UmbModalManagerContext; + #pickedItemsObserver?: UmbObserverController>; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => (this.#modalContext = instance)); + } + + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.#observePickedDocuments(); + } + + protected getFormElement() { + return undefined; + } + + async #observePickedDocuments() { + this.#pickedItemsObserver?.destroy(); + + const { asObservable } = await this.#documentRepository.requestItems(this._selectedIds); + this.#pickedItemsObserver = this.observe(asObservable(), (items) => (this._items = items)); + } + + #openDocumentPicker() { + // We send a shallow copy(good enough as its just an array of ids) of our this._selectedIds, as we don't want the modal to manipulate our data: + const modalContext = this.#modalContext?.open(UMB_DOCUMENT_PICKER_MODAL, { + selection: [...this._selectedIds], + }); + + modalContext?.onSubmit().then(({ selection }: any) => { + //this.#setSelection(selection); + }); + } + + async #removeItem(item: DocumentItemResponseModel) { + const modalContext = this.#modalContext?.open(UMB_CONFIRM_MODAL, { + color: 'danger', + headline: `Remove ${item.name}?`, + content: 'Are you sure you want to remove this item', + confirmLabel: 'Remove', + }); + + await modalContext?.onSubmit(); + const newSelection = this._selectedIds.filter((value) => value !== item.id); + this.#setSelection(newSelection); + } + + #setSelection(newSelection: Array) { + this.selectedIds = newSelection; + this.dispatchEvent(new UmbChangeEvent()); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this.#pickedItemsObserver?.destroy(); + } + + render() { + return html` + ${this._items?.map((item) => this.#renderItem(item))} + Add + `; + } + + #renderItem(item: DocumentItemResponseModel) { + return html`
Render something here
`; + } + + static styles = [ + css` + #add-button { + width: 100%; + } + `, + ]; +} + +export default UmbInputDocumentGranularPermissionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-document-granular-permission': UmbInputDocumentGranularPermissionElement; + } +} diff --git a/src/packages/documents/documents/components/input-document/input-document.context.ts b/src/packages/documents/documents/components/input-document/input-document.context.ts new file mode 100644 index 0000000000..640649a706 --- /dev/null +++ b/src/packages/documents/documents/components/input-document/input-document.context.ts @@ -0,0 +1,10 @@ +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_DOCUMENT_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { DocumentItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export class UmbDocumentPickerContext extends UmbPickerInputContext { + constructor(host: UmbControllerHostElement) { + super(host, 'Umb.Repository.Document', UMB_DOCUMENT_PICKER_MODAL); + } +} diff --git a/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/packages/documents/documents/components/input-document/input-document.element.ts index 210ebfd7db..01df812944 100644 --- a/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -1,16 +1,8 @@ -import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../../repository/document.tree.store.js'; -import type { UmbDocumentTreeStore } from '../../repository/document.tree.store.js'; -import { css, html, nothing, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDocumentPickerContext } from './input-document.context.js'; +import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; -import { - UmbModalManagerContext, - UMB_MODAL_MANAGER_CONTEXT_TOKEN, - UMB_CONFIRM_MODAL, - UMB_DOCUMENT_PICKER_MODAL, -} from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { DocumentTreeItemResponseModel, EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; @customElement('umb-input-document') export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { @@ -18,10 +10,15 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { * This is a minimum amount of selected items in this input. * @type {number} * @attr - * @default undefined + * @default 0 */ @property({ type: Number }) - min?: number; + public get min(): number { + return this.#pickerContext.min; + } + public set min(value: number) { + this.#pickerContext.min = value; + } /** * Min validation message. @@ -36,10 +33,15 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { * This is a maximum amount of selected items in this input. * @type {number} * @attr - * @default undefined + * @default Infinity */ @property({ type: Number }) - max?: number; + public get max(): number { + return this.#pickerContext.max; + } + public set max(value: number) { + this.#pickerContext.max = value; + } /** * Max validation message. @@ -50,30 +52,23 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { @property({ type: String, attribute: 'min-message' }) maxMessage = 'This field exceeds the allowed amount of items'; - // TODO: do we need both selectedIds and value? If we just use value we follow the same pattern as native form controls. - private _selectedIds: Array = []; public get selectedIds(): Array { - return this._selectedIds; + return this.#pickerContext.getSelection(); } public set selectedIds(ids: Array) { - this._selectedIds = ids; - super.value = ids.join(','); - this._observePickedDocuments(); + this.#pickerContext.setSelection(ids); } @property() public set value(idsString: string) { - if (idsString !== this._value) { - this.selectedIds = idsString.split(/[ ,]+/); - } + // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. + this.selectedIds = idsString.split(/[ ,]+/); } @state() - private _items?: Array; + private _items?: Array; - private _modalContext?: UmbModalManagerContext; - private _documentStore?: UmbDocumentTreeStore; - private _pickedItemsObserver?: UmbObserverController; + #pickerContext = new UmbDocumentPickerContext(this); constructor() { super(); @@ -81,84 +76,43 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { this.addValidator( 'rangeUnderflow', () => this.minMessage, - () => !!this.min && this._selectedIds.length < this.min + () => !!this.min && this.#pickerContext.getSelection().length < this.min, ); + this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this._selectedIds.length > this.max + () => !!this.max && this.#pickerContext.getSelection().length > this.max, ); - this.consumeContext(UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN, (instance) => { - this._documentStore = instance; - this._observePickedDocuments(); - }); - this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; - }); + this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); } protected getFormElement() { return undefined; } - private _observePickedDocuments() { - this._pickedItemsObserver?.destroy(); - - if (!this._documentStore) return; - - // TODO: consider changing this to the list data endpoint when it is available - this._pickedItemsObserver = this.observe(this._documentStore.items(this._selectedIds), (items) => { - this._items = items; - }); - } - - private _openPicker() { - // We send a shallow copy(good enough as its just an array of ids) of our this._selectedIds, as we don't want the modal to manipulate our data: - const modalContext = this._modalContext?.open(UMB_DOCUMENT_PICKER_MODAL, { - multiple: this.max === 1 ? false : true, - selection: [...this._selectedIds], - }); - - modalContext?.onSubmit().then(({ selection }: any) => { - this._setSelection(selection); - }); - } - - private async _removeItem(item: EntityTreeItemResponseModel) { - const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, { - color: 'danger', - headline: `Remove ${item.name}?`, - content: 'Are you sure you want to remove this item', - confirmLabel: 'Remove', - }); - - await modalContext?.onSubmit(); - const newSelection = this._selectedIds.filter((value) => value !== item.id); - this._setSelection(newSelection); - } - - private _setSelection(newSelection: Array) { - this.selectedIds = newSelection; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); - } - render() { return html` - ${this._items?.map((item) => this._renderItem(item))} - Add + ${this._items?.map((item) => this._renderItem(item))} + this.#pickerContext.openPicker()} label="open" + >Add `; } - private _renderItem(item: EntityTreeItemResponseModel) { - // TODO: remove when we have a way to handle trashed items - const tempItem = item as EntityTreeItemResponseModel & { isTrashed: boolean }; - + private _renderItem(item: DocumentItemResponseModel) { + if (!item.id) return; return html` - - ${tempItem.isTrashed ? html` Trashed ` : nothing} + + - this._removeItem(item)} label="Remove document ${item.name}">Remove + this.#pickerContext.requestRemoveItem(item.id!)} + label="Remove document ${item.name}" + >Remove `; diff --git a/src/packages/documents/documents/entity-actions/create/manifests.ts b/src/packages/documents/documents/entity-actions/create/manifests.ts index 65b4364c99..99a21a0d53 100644 --- a/src/packages/documents/documents/entity-actions/create/manifests.ts +++ b/src/packages/documents/documents/entity-actions/create/manifests.ts @@ -16,6 +16,15 @@ const entityActions: Array = [ api: UmbCreateDocumentEntityAction, entityTypes: [DOCUMENT_ROOT_ENTITY_TYPE, DOCUMENT_ENTITY_TYPE], }, + conditions: [ + { + alias: 'Umb.Condition.UserPermission', + // TODO: investigate why the match property is not typed + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + match: 'Umb.UserPermission.Document.Create', + }, + ], }, ]; diff --git a/src/packages/documents/documents/entity-actions/permissions.action.ts b/src/packages/documents/documents/entity-actions/permissions.action.ts index 36f3549ca1..b7f53612c2 100644 --- a/src/packages/documents/documents/entity-actions/permissions.action.ts +++ b/src/packages/documents/documents/entity-actions/permissions.action.ts @@ -1,14 +1,41 @@ -import { UmbDocumentRepository } from '../repository/document.repository.js'; +import { type UmbDocumentRepository } from '../repository/document.repository.js'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { + UMB_MODAL_MANAGER_CONTEXT_TOKEN, + UmbModalManagerContext, + UMB_ENTITY_USER_PERMISSION_MODAL, +} from '@umbraco-cms/backoffice/modal'; export class UmbDocumentPermissionsEntityAction extends UmbEntityActionBase { + #modalContext?: UmbModalManagerContext; + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { super(host, repositoryAlias, unique); + + new UmbContextConsumerController(this.host, UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { + this.#modalContext = instance; + }); } async execute() { - console.log(`execute for: ${this.unique}`); - await this.repository?.setPermissions(); + alert('WIP Permissions dialog'); + if (!this.repository) return; + if (!this.#modalContext) return; + + // TODO: we don't get "type" as part of the item + //const { data, error } = await this.repository.requestItems([this.unique]); + + /* + const modalContext = this.#modalContext.open(UMB_ENTITY_USER_PERMISSION_MODAL, { + unique: this.unique, + entityType: ['document'], + }); + */ + + // const { selection } = await modalContext.onSubmit(); + // console.log(selection); + // await this.repository?.setPermissions(); } } diff --git a/src/packages/documents/documents/index.ts b/src/packages/documents/documents/index.ts index 661e019300..cce7f7a361 100644 --- a/src/packages/documents/documents/index.ts +++ b/src/packages/documents/documents/index.ts @@ -1,6 +1,8 @@ export * from './repository/index.js'; export * from './workspace/index.js'; export * from './recycle-bin/index.js'; +export * from './user-permissions/index.js'; +export * from './components/index.js'; import './components/index.js'; diff --git a/src/packages/documents/documents/manifests.ts b/src/packages/documents/documents/manifests.ts index c9fd54a5da..a68a6c65b6 100644 --- a/src/packages/documents/documents/manifests.ts +++ b/src/packages/documents/documents/manifests.ts @@ -6,6 +6,7 @@ import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; +import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; export const manifests = [ @@ -17,5 +18,6 @@ export const manifests = [ ...entityActionManifests, ...entityBulkActionManifests, ...propertyEditorManifests, + ...userPermissionManifests, ...recycleBinManifests, ]; diff --git a/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index 50e4624dd0..e46239cc87 100644 --- a/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -15,5 +15,11 @@ export const manifests = [ api: UmbTrashEntityAction, entityTypes: [DOCUMENT_ENTITY_TYPE], }, + conditions: [ + { + alias: 'Umb.Condition.UserPermission', + match: 'Umb.UserPermission.Document.Delete', + }, + ], }, ]; diff --git a/src/packages/documents/documents/repository/document.repository.ts b/src/packages/documents/documents/repository/document.repository.ts index 01331c1443..b2a7216326 100644 --- a/src/packages/documents/documents/repository/document.repository.ts +++ b/src/packages/documents/documents/repository/document.repository.ts @@ -46,19 +46,19 @@ export class UmbDocumentRepository this.#init = Promise.all([ new UmbContextConsumerController(this.#host, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN, (instance) => { this.#treeStore = instance; - }), + }).asPromise(), new UmbContextConsumerController(this.#host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN, (instance) => { this.#store = instance; - }), + }).asPromise(), new UmbContextConsumerController(this.#host, UMB_DOCUMENT_ITEM_STORE_CONTEXT_TOKEN, (instance) => { this.#itemStore = instance; - }), + }).asPromise(), new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { this.#notificationContext = instance; - }), + }).asPromise(), ]); } diff --git a/src/packages/documents/documents/repository/manifests.ts b/src/packages/documents/documents/repository/manifests.ts index a66e493a3a..adcaa16ab0 100644 --- a/src/packages/documents/documents/repository/manifests.ts +++ b/src/packages/documents/documents/repository/manifests.ts @@ -1,7 +1,13 @@ import { UmbDocumentRepository } from '../repository/document.repository.js'; +import { UmbDocumentItemStore } from './document-item.store.js'; import { UmbDocumentStore } from './document.store.js'; import { UmbDocumentTreeStore } from './document.tree.store.js'; -import type { ManifestRepository, ManifestStore, ManifestTreeStore } from '@umbraco-cms/backoffice/extension-registry'; +import type { + ManifestItemStore, + ManifestRepository, + ManifestStore, + ManifestTreeStore, +} from '@umbraco-cms/backoffice/extension-registry'; export const DOCUMENT_REPOSITORY_ALIAS = 'Umb.Repository.Document'; @@ -14,6 +20,7 @@ const repository: ManifestRepository = { export const DOCUMENT_STORE_ALIAS = 'Umb.Store.Document'; export const DOCUMENT_TREE_STORE_ALIAS = 'Umb.Store.DocumentTree'; +export const DOCUMENT_ITEM_STORE_ALIAS = 'Umb.Store.DocumentItem'; const store: ManifestStore = { type: 'store', @@ -29,4 +36,11 @@ const treeStore: ManifestTreeStore = { class: UmbDocumentTreeStore, }; -export const manifests = [repository, store, treeStore]; +const itemStore: ManifestItemStore = { + type: 'itemStore', + alias: DOCUMENT_ITEM_STORE_ALIAS, + name: 'Document Item Store', + class: UmbDocumentItemStore, +}; + +export const manifests = [repository, store, treeStore, itemStore]; diff --git a/src/packages/documents/documents/user-permissions/index.ts b/src/packages/documents/documents/user-permissions/index.ts new file mode 100644 index 0000000000..1e95b5d703 --- /dev/null +++ b/src/packages/documents/documents/user-permissions/index.ts @@ -0,0 +1 @@ +export * from './manifests.js'; diff --git a/src/packages/documents/documents/user-permissions/manifests.ts b/src/packages/documents/documents/user-permissions/manifests.ts new file mode 100644 index 0000000000..f3269f9701 --- /dev/null +++ b/src/packages/documents/documents/user-permissions/manifests.ts @@ -0,0 +1,205 @@ +import type { + ManifestUserGranularPermission, + ManifestUserPermission, +} from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_USER_PERMISSION_DOCUMENT_CREATE = 'Umb.UserPermission.Document.Create'; +export const UMB_USER_PERMISSION_DOCUMENT_READ = 'Umb.UserPermission.Document.Read'; +export const UMB_USER_PERMISSION_DOCUMENT_UPDATE = 'Umb.UserPermission.Document.Update'; +export const UMB_USER_PERMISSION_DOCUMENT_DELETE = 'Umb.UserPermission.Document.Delete'; +export const UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT = 'Umb.UserPermission.Document.CreateBlueprint'; +export const UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS = 'Umb.UserPermission.Document.Notifications'; +export const UMB_USER_PERMISSION_DOCUMENT_PUBLISH = 'Umb.UserPermission.Document.Publish'; +export const UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS = 'Umb.UserPermission.Document.Permissions'; +export const UMB_USER_PERMISSION_DOCUMENT_SEND_FOR_APPROVAL = 'Umb.UserPermission.Document.SendForApproval'; +export const UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH = 'Umb.UserPermission.Document.Unpublish'; +export const UMB_USER_PERMISSION_DOCUMENT_COPY = 'Umb.UserPermission.Document.Copy'; +export const UMB_USER_PERMISSION_DOCUMENT_MOVE = 'Umb.UserPermission.Document.Move'; +export const UMB_USER_PERMISSION_DOCUMENT_SORT = 'Umb.UserPermission.Document.Sort'; +export const UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES = 'Umb.UserPermission.Document.CultureAndHostnames'; +export const UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS = 'Umb.UserPermission.Document.PublicAccess'; +export const UMB_USER_PERMISSION_DOCUMENT_ROLLBACK = 'Umb.UserPermission.Document.Rollback'; + +const permissions: Array = [ + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_READ, + name: 'Read Document User Permission', + meta: { + entityType: 'document', + label: 'Read', + description: 'Allow access to browse documents', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, + name: 'Create Document Blueprint User Permission', + meta: { + entityType: 'document', + label: 'Create Content Template', + description: 'Allow access to create a Content Template', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_DELETE, + name: 'Delete Document User Permission', + meta: { + entityType: 'document', + label: 'Delete', + description: 'Allow access to delete a document', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_CREATE, + name: 'Create Document User Permission', + meta: { + entityType: 'document', + label: 'Create', + description: 'Allow access to create a document', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, + name: 'Document Notifications User Permission', + meta: { + entityType: 'document', + label: 'Notifications', + description: 'Allow access to setup notifications for documents', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_PUBLISH, + name: 'Publish Document User Permission', + meta: { + entityType: 'document', + label: 'Publish', + description: 'Allow access to publish a document', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, + name: 'Document Permissions User Permission', + meta: { + entityType: 'document', + label: 'Permissions', + description: 'Allow access to change permissions for a document', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_SEND_FOR_APPROVAL, + name: 'Send Document For Approval User Permission', + meta: { + entityType: 'document', + label: 'Send For Approval', + description: 'Allow access to send a document for approval before publishing', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, + name: 'Unpublish Document User Permission', + meta: { + entityType: 'document', + label: 'Unpublish', + description: 'Allow access to unpublish a document', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_UPDATE, + name: 'Update Document User Permission', + meta: { + entityType: 'document', + label: 'Update', + description: 'Allow access to save a document', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_COPY, + name: 'Copy Document User Permission', + meta: { + entityType: 'document', + label: 'Copy', + description: 'Allow access to copy a document', + group: 'structure', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_MOVE, + name: 'Move Document User Permission', + meta: { + entityType: 'document', + label: 'Move', + description: 'Allow access to move a document', + group: 'structure', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_SORT, + name: 'Sort Document User Permission', + meta: { + entityType: 'document', + label: 'Sort', + description: 'Allow access to sort documents', + group: 'structure', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, + name: 'Document Culture And Hostnames User Permission', + meta: { + entityType: 'document', + label: 'Culture And Hostnames', + description: 'Allow access to set culture and hostnames for documents', + group: 'administration', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, + name: 'Document Public Access User Permission', + meta: { + entityType: 'document', + label: 'Public Access', + description: 'Allow access to set and change access restrictions for a document', + group: 'administration', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, + name: 'Document Rollback User Permission', + meta: { + entityType: 'document', + label: 'Rollback', + description: 'Allow access to roll back a document to a previous state', + group: 'administration', + }, + }, +]; + +export const granularPermissions: Array = [ + { + type: 'userGranularPermission', + alias: 'Umb.UserGranularPermission.Document', + name: 'Document Granular User Permission', + loader: () => + import('../components/input-document-granular-permission/input-document-granular-permission.element.js'), + meta: { + entityType: 'document', + }, + }, +]; + +export const manifests = [...permissions, ...granularPermissions]; diff --git a/src/packages/media/manifests.ts b/src/packages/media/manifests.ts index 138fdd7f88..940d82b7ca 100644 --- a/src/packages/media/manifests.ts +++ b/src/packages/media/manifests.ts @@ -3,4 +3,6 @@ import { manifests as mediaMenuManifests } from './menu.manifests.js'; import { manifests as mediaManifests } from './media/manifests.js'; import { manifests as mediaTypesManifests } from './media-types/manifests.js'; +import './media/components/index.js'; + export const manifests = [...mediaSectionManifests, ...mediaMenuManifests, ...mediaManifests, ...mediaTypesManifests]; diff --git a/src/packages/media/media/components/index.ts b/src/packages/media/media/components/index.ts index f744600b4e..b0cd856c7d 100644 --- a/src/packages/media/media/components/index.ts +++ b/src/packages/media/media/components/index.ts @@ -1 +1,3 @@ import './input-media/input-media.element.js'; + +export * from './input-media/input-media.element.js'; diff --git a/src/packages/media/media/components/input-media/input-media.context.ts b/src/packages/media/media/components/input-media/input-media.context.ts new file mode 100644 index 0000000000..cb668fd2f2 --- /dev/null +++ b/src/packages/media/media/components/input-media/input-media.context.ts @@ -0,0 +1,10 @@ +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_MEDIA_TREE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { MediaItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export class UmbMediaPickerContext extends UmbPickerInputContext { + constructor(host: UmbControllerHostElement) { + super(host, 'Umb.Repository.Media', UMB_MEDIA_TREE_PICKER_MODAL); + } +} diff --git a/src/packages/media/media/components/input-media/input-media.element.ts b/src/packages/media/media/components/input-media/input-media.element.ts index a6cc059d08..193cce9c7c 100644 --- a/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/packages/media/media/components/input-media/input-media.element.ts @@ -1,15 +1,8 @@ -import { UmbMediaRepository } from '../../repository/media.repository.js'; -import { css, html, nothing, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbMediaPickerContext } from './input-media.context.js'; +import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; -import { - UmbModalManagerContext, - UMB_MODAL_MANAGER_CONTEXT_TOKEN, - UMB_CONFIRM_MODAL, - UMB_MEDIA_TREE_PICKER_MODAL, -} from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import type { MediaItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; @customElement('umb-input-media') export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { @@ -17,10 +10,15 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { * This is a minimum amount of selected items in this input. * @type {number} * @attr - * @default undefined + * @default 0 */ @property({ type: Number }) - min?: number; + public get min(): number { + return this.#pickerContext.min; + } + public set min(value: number) { + this.#pickerContext.min = value; + } /** * Min validation message. @@ -35,10 +33,15 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { * This is a maximum amount of selected items in this input. * @type {number} * @attr - * @default undefined + * @default Infinity */ @property({ type: Number }) - max?: number; + public get max(): number { + return this.#pickerContext.max; + } + public set max(value: number) { + this.#pickerContext.max = value; + } /** * Max validation message. @@ -49,30 +52,23 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { @property({ type: String, attribute: 'min-message' }) maxMessage = 'This field exceeds the allowed amount of items'; - // TODO: do we need both selectedIds and value? If we just use value we follow the same pattern as native form controls. - private _selectedIds: Array = []; public get selectedIds(): Array { - return this._selectedIds; + return this.#pickerContext.getSelection(); } public set selectedIds(ids: Array) { - this._selectedIds = ids; - super.value = ids.join(','); - this._observePickedMedias(); + this.#pickerContext.setSelection(ids); } @property() public set value(idsString: string) { - if (idsString !== this._value) { - this.selectedIds = idsString.split(/[ ,]+/); - } + // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. + this.selectedIds = idsString.split(/[ ,]+/); } @state() - private _items?: Array; + private _items?: Array; - private _modalContext?: UmbModalManagerContext; - private _pickedItemsObserver?: UmbObserverController; - private _repository = new UmbMediaRepository(this); + #pickerContext = new UmbMediaPickerContext(this); constructor() { super(); @@ -80,104 +76,54 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { this.addValidator( 'rangeUnderflow', () => this.minMessage, - () => !!this.min && this._selectedIds.length < this.min + () => !!this.min && this.#pickerContext.getSelection().length < this.min, ); + this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this._selectedIds.length > this.max + () => !!this.max && this.#pickerContext.getSelection().length > this.max, ); - this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; - }); - } - - connectedCallback(): void { - super.connectedCallback(); - this._observePickedMedias(); + this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); } protected getFormElement() { return undefined; } - private async _observePickedMedias() { - this._pickedItemsObserver?.destroy(); - - // TODO: consider changing this to the list data endpoint when it is available - const { asObservable } = await this._repository.requestItemsLegacy(this._selectedIds); - - if (!asObservable) return; - - this._pickedItemsObserver = this.observe(asObservable(), (items) => { - this._items = items; - }); - } - - private _openPicker() { - // We send a shallow copy(good enough as its just an array of ids) of our this._selectedIds, as we don't want the modal to manipulate our data: - const modalContext = this._modalContext?.open(UMB_MEDIA_TREE_PICKER_MODAL, { - multiple: this.max === 1 ? false : true, - selection: [...this._selectedIds], - }); - - modalContext?.onSubmit().then(({ selection }: any) => { - this._setSelection(selection); - }); - } - - private _removeItem(item: EntityTreeItemResponseModel) { - const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, { - color: 'danger', - headline: `Remove ${item.name}?`, - content: 'Are you sure you want to remove this item', - confirmLabel: 'Remove', - }); - - modalContext?.onSubmit().then(() => { - const newSelection = this._selectedIds.filter((value) => value !== item.id); - this._setSelection(newSelection); - }); - } - - private _setSelection(newSelection: Array) { - this.selectedIds = newSelection; - this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })); - } - render() { - return html` ${this._items?.map((item) => this._renderItem(item))} ${this._renderButton()} `; + return html` ${this._items?.map((item) => this.#renderItem(item))} ${this.#renderButton()} `; } - private _renderButton() { + + #renderButton() { if (this._items && this.max && this._items.length >= this.max) return; - return html` - - Add - `; + return html` + this.#pickerContext.openPicker()} label="open"> + + Add + + `; } - private _renderItem(item: EntityTreeItemResponseModel) { - // TODO: remove when we have a way to handle trashed items - const tempItem = item as EntityTreeItemResponseModel & { isTrashed: boolean }; - + #renderItem(item: MediaItemResponseModel) { return html` - ${tempItem.isTrashed ? html` Trashed ` : nothing} + - this._removeItem(item)} label="Remove media ${item.name}"> + this.#pickerContext.requestRemoveItem(item.id!)} label="Remove media ${item.name}"> `; - //TODO: } static styles = [ @@ -187,6 +133,7 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { gap: var(--uui-size-space-3); grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } + #add-button { text-align: center; height: 160px; diff --git a/src/packages/media/media/index.ts b/src/packages/media/media/index.ts index 203cdbb451..acd16c9b4f 100644 --- a/src/packages/media/media/index.ts +++ b/src/packages/media/media/index.ts @@ -1,5 +1,7 @@ import { ContentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +export * from './components/index.js'; + // Content export interface ContentProperty { alias: string; @@ -21,4 +23,5 @@ export interface MediaDetails extends ContentTreeItemResponseModel { data: Array; variants: Array; // TODO: define variant data //layout?: any; // TODO: define layout type - make it non-optional + icon?: string; } diff --git a/src/packages/media/media/manifests.ts b/src/packages/media/media/manifests.ts index cf5eb52e5f..43a93dafbb 100644 --- a/src/packages/media/media/manifests.ts +++ b/src/packages/media/media/manifests.ts @@ -5,6 +5,7 @@ import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; +import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; export const manifests = [ ...collectionViewManifests, @@ -14,4 +15,5 @@ export const manifests = [ ...workspaceManifests, ...entityActionsManifests, ...entityBulkActionsManifests, + ...userPermissionManifests, ]; diff --git a/src/packages/media/media/repository/manifests.ts b/src/packages/media/media/repository/manifests.ts index 5e2530f6b1..1602d61aac 100644 --- a/src/packages/media/media/repository/manifests.ts +++ b/src/packages/media/media/repository/manifests.ts @@ -1,7 +1,13 @@ +import { UmbMediaItemStore } from './media-item.store.js'; import { UmbMediaRepository } from './media.repository.js'; import { UmbMediaStore } from './media.store.js'; import { UmbMediaTreeStore } from './media.tree.store.js'; -import type { ManifestStore, ManifestTreeStore, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; +import type { + ManifestStore, + ManifestTreeStore, + ManifestRepository, + ManifestItemStore, +} from '@umbraco-cms/backoffice/extension-registry'; export const MEDIA_REPOSITORY_ALIAS = 'Umb.Repository.Media'; @@ -14,6 +20,7 @@ const repository: ManifestRepository = { export const MEDIA_STORE_ALIAS = 'Umb.Store.Media'; export const MEDIA_TREE_STORE_ALIAS = 'Umb.Store.MediaTree'; +export const MEDIA_ITEM_STORE_ALIAS = 'Umb.Store.MediaItem'; const store: ManifestStore = { type: 'store', @@ -29,4 +36,11 @@ const treeStore: ManifestTreeStore = { class: UmbMediaTreeStore, }; -export const manifests = [store, treeStore, repository]; +const itemStore: ManifestItemStore = { + type: 'itemStore', + alias: MEDIA_ITEM_STORE_ALIAS, + name: 'Media Item Store', + class: UmbMediaItemStore, +}; + +export const manifests = [store, treeStore, itemStore, repository]; diff --git a/src/packages/media/media/repository/media-item.store.ts b/src/packages/media/media/repository/media-item.store.ts new file mode 100644 index 0000000000..87578dcbb7 --- /dev/null +++ b/src/packages/media/media/repository/media-item.store.ts @@ -0,0 +1,36 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemStore, UmbStoreBase } from '@umbraco-cms/backoffice/store'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { MediaItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +/** + * @export + * @class UmbMediaItemStore + * @extends {UmbStoreBase} + * @description - Data Store for Media items + */ + +export class UmbMediaItemStore + extends UmbStoreBase + implements UmbItemStore +{ + /** + * Creates an instance of UmbMediaItemStore. + * @param {UmbControllerHostElement} host + * @memberof UmbMediaItemStore + */ + constructor(host: UmbControllerHostElement) { + super( + host, + UMB_Media_ITEM_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (x) => x.id), + ); + } + + items(ids: Array) { + return this._data.asObservablePart((items) => items.filter((item) => ids.includes(item.id ?? ''))); + } +} + +export const UMB_Media_ITEM_STORE_CONTEXT_TOKEN = new UmbContextToken('UmbMediaItemStore'); diff --git a/src/packages/media/media/repository/media.repository.ts b/src/packages/media/media/repository/media.repository.ts index 64730ae059..0f27f0c7b5 100644 --- a/src/packages/media/media/repository/media.repository.ts +++ b/src/packages/media/media/repository/media.repository.ts @@ -3,6 +3,8 @@ import { UmbMediaTreeServerDataSource } from './sources/media.tree.server.data.j import { UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media.tree.store.js'; import { UmbMediaStore, UMB_MEDIA_STORE_CONTEXT_TOKEN } from './media.store.js'; import { UmbMediaDetailServerDataSource } from './sources/media.detail.server.data.js'; +import { UmbMediaItemServerDataSource } from './sources/media-item.server.data.js'; +import { UmbMediaItemStore } from './media-item.store.js'; import type { UmbTreeRepository, UmbTreeDataSource } from '@umbraco-cms/backoffice/repository'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; @@ -28,6 +30,9 @@ export class UmbMediaRepository #detailDataSource: UmbMediaDetailServerDataSource; #store?: UmbMediaStore; + #itemSource: UmbMediaItemServerDataSource; + #itemStore?: UmbMediaItemStore; + #notificationContext?: UmbNotificationContext; constructor(host: UmbControllerHostElement) { @@ -36,6 +41,7 @@ export class UmbMediaRepository // TODO: figure out how spin up get the correct data source this.#treeSource = new UmbMediaTreeServerDataSource(this.#host); this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host); + this.#itemSource = new UmbMediaItemServerDataSource(this.#host); this.#init = Promise.all([ new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { @@ -46,6 +52,10 @@ export class UmbMediaRepository this.#store = instance; }).asPromise(), + new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#itemStore = instance; + }).asPromise(), + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { this.#notificationContext = instance; }).asPromise(), @@ -119,6 +129,25 @@ export class UmbMediaRepository return this.#treeStore!.items(ids); } + // ITEMS: + async requestItems(ids: Array) { + if (!ids) throw new Error('Keys are missing'); + await this.#init; + + const { data, error } = await this.#itemSource.getItems(ids); + + if (data) { + this.#itemStore?.appendItems(data); + } + + return { data, error, asObservable: () => this.#itemStore!.items(ids) }; + } + + async items(ids: Array) { + await this.#init; + return this.#itemStore!.items(ids); + } + // DETAILS: async createScaffold(parentId: string | null) { diff --git a/src/packages/media/media/repository/sources/media-item.server.data.ts b/src/packages/media/media/repository/sources/media-item.server.data.ts new file mode 100644 index 0000000000..7457bcd79f --- /dev/null +++ b/src/packages/media/media/repository/sources/media-item.server.data.ts @@ -0,0 +1,38 @@ +import type { UmbItemDataSource } from '@umbraco-cms/backoffice/repository'; +import { MediaResource, MediaItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A data source for Media items that fetches data from the server + * @export + * @class UmbMediaItemServerDataSource + * @implements {MediaItemDataSource} + */ +export class UmbMediaItemServerDataSource implements UmbItemDataSource { + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbMediaItemServerDataSource. + * @param {UmbControllerHostElement} host + * @memberof UmbMediaItemServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + /** + * Fetches the items for the given ids from the server + * @param {Array} ids + * @memberof UmbMediaItemServerDataSource + */ + async getItems(ids: Array) { + if (!ids) throw new Error('Ids are missing'); + return tryExecuteAndNotify( + this.#host, + MediaResource.getMediaItem({ + id: ids, + }), + ); + } +} diff --git a/src/packages/media/media/user-permissions/index.ts b/src/packages/media/media/user-permissions/index.ts new file mode 100644 index 0000000000..1e95b5d703 --- /dev/null +++ b/src/packages/media/media/user-permissions/index.ts @@ -0,0 +1 @@ +export * from './manifests.js'; diff --git a/src/packages/media/media/user-permissions/manifests.ts b/src/packages/media/media/user-permissions/manifests.ts new file mode 100644 index 0000000000..3ddbb9b526 --- /dev/null +++ b/src/packages/media/media/user-permissions/manifests.ts @@ -0,0 +1,31 @@ +import type { ManifestUserPermission } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_USER_PERMISSION_MEDIA_MOVE = 'Umb.UserPermission.Media.Move'; +export const UMB_USER_PERMISSION_MEDIA_COPY = 'Umb.UserPermission.Media.Copy'; + +const permissions: Array = [ + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_MEDIA_MOVE, + name: 'Move Media Item User Permission', + meta: { + entityType: 'media', + label: 'Move', + description: 'Allow access to move media items', + group: 'structure', + }, + }, + { + type: 'userPermission', + alias: UMB_USER_PERMISSION_MEDIA_COPY, + name: 'Copy Media Item User Permission', + meta: { + entityType: 'media', + label: 'Copy', + description: 'Allow access to copy a media item', + group: 'structure', + }, + }, +]; + +export const manifests = [...permissions]; diff --git a/src/packages/users/current-user/conditions/index.ts b/src/packages/users/current-user/conditions/index.ts new file mode 100644 index 0000000000..64b786466c --- /dev/null +++ b/src/packages/users/current-user/conditions/index.ts @@ -0,0 +1 @@ +export * from './user-permission.condition.js'; diff --git a/src/packages/users/current-user/conditions/user-permission.condition.ts b/src/packages/users/current-user/conditions/user-permission.condition.ts new file mode 100644 index 0000000000..1beb530856 --- /dev/null +++ b/src/packages/users/current-user/conditions/user-permission.condition.ts @@ -0,0 +1,44 @@ +import { UMB_AUTH } from '@umbraco-cms/backoffice/auth'; +import { UmbBaseController } from '@umbraco-cms/backoffice/controller-api'; +import { + ManifestCondition, + UmbConditionConfigBase, + UmbConditionControllerArguments, + UmbExtensionCondition, +} from '@umbraco-cms/backoffice/extension-api'; + +export class UmbUserPermissionCondition extends UmbBaseController implements UmbExtensionCondition { + config: UserPermissionConditionConfig; + permitted = false; + #onChange: () => void; + + constructor(args: UmbConditionControllerArguments) { + super(args.host); + this.config = args.config; + this.#onChange = args.onChange; + + this.consumeContext(UMB_AUTH, (context) => { + this.observe(context.currentUser, (currentUser) => { + this.permitted = currentUser?.permissions?.includes(this.config.match) || false; + this.#onChange(); + }); + }); + } +} + +export type UserPermissionConditionConfig = UmbConditionConfigBase<'Umb.Condition.UserPermission'> & { + /** + * + * + * @example + * "Umb.UserPermission.Document.Create" + */ + match: string; +}; + +export const manifest: ManifestCondition = { + type: 'condition', + name: 'User Permission Condition', + alias: 'Umb.Condition.UserPermission', + class: UmbUserPermissionCondition, +}; diff --git a/src/packages/users/current-user/index.ts b/src/packages/users/current-user/index.ts index 0b68da794d..4cbce7fc3e 100644 --- a/src/packages/users/current-user/index.ts +++ b/src/packages/users/current-user/index.ts @@ -1,2 +1,3 @@ // TODO:Do not export store, but instead export future repository export * from './current-user-history.store.js'; +export * from './conditions/index.js'; diff --git a/src/packages/users/current-user/manifests.ts b/src/packages/users/current-user/manifests.ts index f6fe418913..8765bc4d95 100644 --- a/src/packages/users/current-user/manifests.ts +++ b/src/packages/users/current-user/manifests.ts @@ -1,5 +1,6 @@ import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as userProfileAppsManifests } from './user-profile-apps/manifests.js'; +import { manifest as userPermissionConditionManifest } from './conditions/user-permission.condition.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const headerApps: Array = [ @@ -23,4 +24,9 @@ export const headerApps: Array = [ }, ]; -export const manifests = [...headerApps, ...modalManifests, ...userProfileAppsManifests]; +export const manifests = [ + ...headerApps, + ...modalManifests, + ...userProfileAppsManifests, + userPermissionConditionManifest, +]; diff --git a/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts b/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts index c3c5651e0f..809eb5715a 100644 --- a/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts +++ b/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts @@ -13,7 +13,7 @@ export class UmbCurrentUserModalElement extends UmbLitElement { @state() private _currentUser?: UmbLoggedInUser; - #auth?: typeof UMB_AUTH.TYPE; + #authContext?: typeof UMB_AUTH.TYPE; #appContext?: typeof UMB_APP.TYPE; @@ -21,7 +21,7 @@ export class UmbCurrentUserModalElement extends UmbLitElement { super(); this.consumeContext(UMB_AUTH, (instance) => { - this.#auth = instance; + this.#authContext = instance; this._observeCurrentUser(); }); @@ -31,9 +31,9 @@ export class UmbCurrentUserModalElement extends UmbLitElement { } private async _observeCurrentUser() { - if (!this.#auth) return; + if (!this.#authContext) return; - this.observe(this.#auth.currentUser, (currentUser) => { + this.observe(this.#authContext.currentUser, (currentUser) => { this._currentUser = currentUser; }); } @@ -43,9 +43,9 @@ export class UmbCurrentUserModalElement extends UmbLitElement { } private async _logout() { - if (!this.#auth) return; - this.#auth.performWithFreshTokens; - await this.#auth.signOut(); + if (!this.#authContext) return; + this.#authContext.performWithFreshTokens; + await this.#authContext.signOut(); let newUrl = this.#appContext ? `${this.#appContext.getBackofficePath()}/login` : '/'; newUrl = newUrl.replace(/\/\//g, '/'); location.href = newUrl; diff --git a/src/packages/users/index.ts b/src/packages/users/index.ts index 8fb22d2dd6..2f1673f76b 100644 --- a/src/packages/users/index.ts +++ b/src/packages/users/index.ts @@ -1,2 +1,4 @@ -export * from './users/components/index.js'; -export * from './user-groups/components/index.js'; +export * from './current-user/index.js'; +export * from './user-groups/index.js'; +export * from './users/index.js'; +export * from './manifests.js'; diff --git a/src/packages/users/manifests.ts b/src/packages/users/manifests.ts index 75aecdcb8b..74b7562a5d 100644 --- a/src/packages/users/manifests.ts +++ b/src/packages/users/manifests.ts @@ -3,4 +3,8 @@ import { manifests as userManifests } from './users/manifests.js'; import { manifests as userSectionManifests } from './user-section/manifests.js'; import { manifests as currentUserManifests } from './current-user/manifests.js'; +// We need to load any components that are not loaded by the user management bundle to register them in the browser. +import './user-groups/components/index.js'; +import './users/components/index.js'; + export const manifests = [...userGroupManifests, ...userManifests, ...userSectionManifests, ...currentUserManifests]; diff --git a/src/packages/users/user-groups/components/index.ts b/src/packages/users/user-groups/components/index.ts index 087396e3a3..11d389d6f6 100644 --- a/src/packages/users/user-groups/components/index.ts +++ b/src/packages/users/user-groups/components/index.ts @@ -1 +1,3 @@ import './input-user-group/user-group-input.element.js'; + +export * from './input-user-group/user-group-input.element.js'; diff --git a/src/packages/users/user-groups/entity-actions/manifests.ts b/src/packages/users/user-groups/entity-actions/manifests.ts new file mode 100644 index 0000000000..0c67c05e58 --- /dev/null +++ b/src/packages/users/user-groups/entity-actions/manifests.ts @@ -0,0 +1,22 @@ +import { USER_GROUP_REPOSITORY_ALIAS } from '../repository/manifests.js'; +import { UMB_USER_GROUP_ENTITY_TYPE } from '../index.js'; +import { UmbDeleteEntityAction } from '@umbraco-cms/backoffice/entity-action'; +import { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.UserGroup.Delete', + name: 'Delete User Group Entity Action', + weight: 900, + meta: { + icon: 'umb:trash', + label: 'Delete...', + repositoryAlias: USER_GROUP_REPOSITORY_ALIAS, + api: UmbDeleteEntityAction, + entityTypes: [UMB_USER_GROUP_ENTITY_TYPE], + }, + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/packages/users/user-groups/index.ts b/src/packages/users/user-groups/index.ts index d4702960d5..81c17a6f51 100644 --- a/src/packages/users/user-groups/index.ts +++ b/src/packages/users/user-groups/index.ts @@ -1 +1,4 @@ export * from './types.js'; +export * from './components/index.js'; + +export const UMB_USER_GROUP_ENTITY_TYPE = 'user-group'; diff --git a/src/packages/users/user-groups/manifests.ts b/src/packages/users/user-groups/manifests.ts index 7772c9b787..5d01d53186 100644 --- a/src/packages/users/user-groups/manifests.ts +++ b/src/packages/users/user-groups/manifests.ts @@ -2,6 +2,7 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as sectionViewManifests } from './section-view/manifests.js'; +import { manifests as entityActionManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js'; export const manifests = [ @@ -9,5 +10,6 @@ export const manifests = [ ...workspaceManifests, ...modalManifests, ...sectionViewManifests, + ...entityActionManifests, ...entityBulkActionManifests, ]; diff --git a/src/packages/users/user-groups/workspace/components/user-group-default-permission-list.element.ts b/src/packages/users/user-groups/workspace/components/user-group-default-permission-list.element.ts new file mode 100644 index 0000000000..b6920fc87c --- /dev/null +++ b/src/packages/users/user-groups/workspace/components/user-group-default-permission-list.element.ts @@ -0,0 +1,83 @@ +import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context.js'; +import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UserGroupResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { ManifestUserPermission, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { groupBy } from '@umbraco-cms/backoffice/external/lodash'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/events'; +import { type UmbUserPermissionSettingElement } from '@umbraco-cms/backoffice/users'; + +@customElement('umb-user-group-default-permission-list') +export class UmbUserGroupDefaultPermissionListElement extends UmbLitElement { + @state() + private _userGroup?: UserGroupResponseModel; + + @state() + private _manifests: Array = []; + + @state() + private _entityTypes: Array = []; + + #userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.#observeUserPermissions(); + + this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => { + this.#userGroupWorkspaceContext = instance; + this.observe(this.#userGroupWorkspaceContext.data, (userGroup) => (this._userGroup = userGroup)); + }); + } + + #observeUserPermissions() { + this.observe(umbExtensionsRegistry.extensionsOfType('userPermission'), (userPermissionManifests) => { + this._manifests = userPermissionManifests; + this._entityTypes = [...new Set(userPermissionManifests.map((manifest) => manifest.meta.entityType))]; + }); + } + + #onChangeUserPermission(event: UmbChangeEvent, userPermissionManifest: ManifestUserPermission) { + const target = event.target as UmbUserPermissionSettingElement; + + target.allowed + ? this.#userGroupWorkspaceContext?.addPermission(userPermissionManifest.alias) + : this.#userGroupWorkspaceContext?.removePermission(userPermissionManifest.alias); + } + + #isAllowed(userPermissionManifest: ManifestUserPermission) { + return this._userGroup?.permissions?.includes(userPermissionManifest.alias); + } + + render() { + return html` ${this._entityTypes.map((entityType) => this.#renderPermissionsForEntityType(entityType))} `; + } + + #renderPermissionsForEntityType(entityType: string) { + return html`

${entityType}

+ ${this._manifests + .filter((manifest) => manifest.meta.entityType === entityType) + .map((manifest) => this.#renderPermission(manifest))}`; + } + + #renderPermission(manifest: ManifestUserPermission) { + return html` + this.#onChangeUserPermission(event, manifest)}>`; + } + + static styles = [UmbTextStyles]; +} + +export default UmbUserGroupDefaultPermissionListElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-group-default-permission-list': UmbUserGroupDefaultPermissionListElement; + } +} diff --git a/src/packages/users/user-groups/workspace/components/user-group-granular-permission-list.element.ts b/src/packages/users/user-groups/workspace/components/user-group-granular-permission-list.element.ts new file mode 100644 index 0000000000..57d87998b1 --- /dev/null +++ b/src/packages/users/user-groups/workspace/components/user-group-granular-permission-list.element.ts @@ -0,0 +1,36 @@ +import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context.js'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UserGroupResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-user-group-granular-permission-list') +export class UmbUserGroupGranularPermissionListElement extends UmbLitElement { + @state() + private _userGroup?: UserGroupResponseModel; + + #workspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.observe(this.#workspaceContext.data, (userGroup) => (this._userGroup = userGroup)); + }); + } + + render() { + return html``; + } + + static styles = [UmbTextStyles, css``]; +} + +export default UmbUserGroupGranularPermissionListElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-group-granular-permission-list': UmbUserGroupGranularPermissionListElement; + } +} diff --git a/src/packages/users/user-groups/workspace/user-group-workspace-editor.element.ts b/src/packages/users/user-groups/workspace/user-group-workspace-editor.element.ts index e66500702f..7b7453b3b2 100644 --- a/src/packages/users/user-groups/workspace/user-group-workspace-editor.element.ts +++ b/src/packages/users/user-groups/workspace/user-group-workspace-editor.element.ts @@ -1,17 +1,18 @@ +import { UMB_USER_GROUP_ENTITY_TYPE } from '../index.js'; import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from './user-group-workspace.context.js'; import { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -// TODO: import from package when available -//import { UmbUserInputElement } from '../../users/components/user-input/user-input.element.js'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; - import { UserGroupResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { - UMB_CONFIRM_MODAL, - UMB_MODAL_MANAGER_CONTEXT_TOKEN, - UmbModalManagerContext, -} from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; +import { UmbInputSectionElement } from '@umbraco-cms/backoffice/components'; +import { UmbUserInputElement } from '@umbraco-cms/backoffice/users'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/events'; +import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; + +import './components/user-group-default-permission-list.element.js'; +import './components/user-group-granular-permission-list.element.js'; @customElement('umb-user-group-workspace-editor') export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { @@ -22,7 +23,6 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { private _userKeys?: Array; #workspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; - #modalContext?: UmbModalManagerContext; constructor() { super(); @@ -32,38 +32,30 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { this.observe(this.#workspaceContext.data, (userGroup) => (this._userGroup = userGroup)); this.observe(this.#workspaceContext.userIds, (userKeys) => (this._userKeys = userKeys)); }); - - this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { - this.#modalContext = instance; - }); } - #onUsersChange(userIds: Array) { - this.#workspaceContext?.updateUserKeys(userIds); + #onSectionsChange(event: UmbChangeEvent) { + event.stopPropagation(); + const target = event.target as UmbInputSectionElement; + this.#workspaceContext?.updateProperty('sections', target.value); } - #onSectionsChange(value: string[]) { - this.#workspaceContext?.updateProperty('sections', value); + #onDocumentStartNodeChange(event: CustomEvent) { + event.stopPropagation(); + const target = event.target as UmbInputDocumentElement; + this.#workspaceContext?.updateProperty('documentStartNodeId', target.selectedIds[0]); } - async #onDelete() { - if (!this.#modalContext || !this.#workspaceContext) return; - - const modalContext = this.#modalContext.open(UMB_CONFIRM_MODAL, { - color: 'danger', - headline: `Delete user group ${this._userGroup?.name}?`, - content: html`Are you sure you want to delete ${this._userGroup?.name} user group?`, - confirmLabel: 'Delete', - }); - - await modalContext.onSubmit(); - - if (!this._userGroup || !this._userGroup.id) return; - - await this.#workspaceContext.delete(this._userGroup?.id); - //TODO: should we check if it actually succeeded in deleting the user group? + #onMediaStartNodeChange(event: CustomEvent) { + event.stopPropagation(); + const target = event.target as UmbInputMediaElement; + this.#workspaceContext?.updateProperty('mediaStartNodeId', target.selectedIds[0]); + } - history.pushState(null, '', 'section/users/view/user-groups'); + #onUsersChange(event: UmbChangeEvent) { + event.stopPropagation(); + const target = event.target as UmbUserInputElement; + this.#workspaceContext?.updateUserKeys(target.selectedIds); } #onNameChange(event: UUIInputEvent) { @@ -108,57 +100,61 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { #renderLeftColumn() { if (!this._userGroup) return nothing; - return html` + return html` +
- + this.#onSectionsChange(e.target.value)}> + @change=${this.#onSectionsChange}> - CONTENT START NODE PICKER NOT IMPLEMENTED YET + - MEDIA START NODE PICKER NOT IMPLEMENTED YET + label=${this.localize.term('defaultdialogs_selectMediaStartNode')} + description=${this.localize.term('user_mediastartnodehelp')}> +
- PERMISSIONS NOT IMPLEMENTED YET +
+ + `; } #renderRightColumn() { return html`
- - this.#onUsersChange((e.target as any).selectedIds)} - .selectedIds=${this._userKeys ?? []}> +
- -
- - -
- -
`; + + `; } static styles = [ diff --git a/src/packages/users/user-groups/workspace/user-group-workspace.context.ts b/src/packages/users/user-groups/workspace/user-group-workspace.context.ts index 04b1193c9a..d750f28faf 100644 --- a/src/packages/users/user-groups/workspace/user-group-workspace.context.ts +++ b/src/packages/users/user-groups/workspace/user-group-workspace.context.ts @@ -55,7 +55,7 @@ export class UmbUserGroupWorkspaceContext getEntityId(): string | undefined { throw new Error('Method not implemented.'); } - + getEntityType(): string { return 'user-group'; } @@ -63,6 +63,7 @@ export class UmbUserGroupWorkspaceContext getData(): UserGroupResponseModel | undefined { throw new Error('Method not implemented.'); } + async save() { if (!this.#data.value) return; @@ -104,10 +105,34 @@ export class UmbUserGroupWorkspaceContext updateUserKeys(keys: Array) { this.#userIds.next(keys); } -} + /** + * Adds a permission to the user group permission array. + * @param {string} permissionAlias + * @memberof UmbUserGroupWorkspaceContext + */ + addPermission(permissionAlias: string) { + const permissions = this.#data.getValue()?.permissions ?? []; + const newValue = [...permissions, permissionAlias]; + this.#data.update({ permissions: newValue }); + } + + /** + * Removes a permission from the user group permission array. + * @param {string} permissionAlias + * @memberof UmbUserGroupWorkspaceContext + */ + removePermission(permissionAlias: string) { + const permissions = this.#data.getValue()?.permissions ?? []; + const newValue = permissions.filter((alias) => alias !== permissionAlias); + this.#data.update({ permissions: newValue }); + } +} -export const UMB_USER_GROUP_WORKSPACE_CONTEXT = new UmbContextToken( +export const UMB_USER_GROUP_WORKSPACE_CONTEXT = new UmbContextToken< + UmbSaveableWorkspaceContextInterface, + UmbUserGroupWorkspaceContext +>( 'UmbWorkspaceContext', - (context): context is UmbUserGroupWorkspaceContext => context.getEntityType?.() === 'user-group' + (context): context is UmbUserGroupWorkspaceContext => context.getEntityType?.() === 'user-group', ); diff --git a/src/packages/users/user-groups/workspace/user-group-workspace.element.ts b/src/packages/users/user-groups/workspace/user-group-workspace.element.ts index d4064d4457..861d9e3ebf 100644 --- a/src/packages/users/user-groups/workspace/user-group-workspace.element.ts +++ b/src/packages/users/user-groups/workspace/user-group-workspace.element.ts @@ -1,6 +1,6 @@ import { UmbUserGroupWorkspaceContext } from './user-group-workspace.context.js'; import { UmbUserGroupWorkspaceEditorElement } from './user-group-workspace-editor.element.js'; -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { UmbRoute } from '@umbraco-cms/backoffice/router'; @@ -22,7 +22,7 @@ export class UmbUserGroupWorkspaceElement extends UmbLitElement { new UmbWorkspaceIsNewRedirectController( this, this.#workspaceContext, - this.shadowRoot!.querySelector('umb-router-slot')! + this.shadowRoot!.querySelector('umb-router-slot')!, ); }, }, diff --git a/src/packages/users/users/components/index.ts b/src/packages/users/users/components/index.ts index 9166992fcc..2a4312053b 100644 --- a/src/packages/users/users/components/index.ts +++ b/src/packages/users/users/components/index.ts @@ -1 +1,5 @@ import './user-input/user-input.element.js'; +import './user-permission-setting/user-permission-setting.element.js'; + +export * from './user-input/user-input.element.js'; +export * from './user-permission-setting/user-permission-setting.element.js'; diff --git a/src/packages/users/users/components/user-input/user-input.element.ts b/src/packages/users/users/components/user-input/user-input.element.ts index 69db7f7311..15d6f274b4 100644 --- a/src/packages/users/users/components/user-input/user-input.element.ts +++ b/src/packages/users/users/components/user-input/user-input.element.ts @@ -1,5 +1,5 @@ import { UmbUserPickerContext } from './user-input.context.js'; -import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { UserItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -76,13 +76,13 @@ export class UmbUserInputElement extends FormControlMixin(UmbLitElement) { this.addValidator( 'rangeUnderflow', () => this.minMessage, - () => !!this.min && this.#pickerContext.getSelection().length < this.min + () => !!this.min && this.#pickerContext.getSelection().length < this.min, ); this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this.#pickerContext.getSelection().length > this.max + () => !!this.max && this.#pickerContext.getSelection().length > this.max, ); this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); @@ -105,7 +105,7 @@ export class UmbUserInputElement extends FormControlMixin(UmbLitElement) { private _renderItem(item: UserItemResponseModel) { if (!item.id) return; return html` - + this.#pickerContext.requestRemoveItem(item.id!)} label="Remove ${item.name}" >Remove + +
+
${this.label}
+ ${this.description} +
+
+ `; + } + + static styles = [ + UmbTextStyles, + css` + #setting { + display: flex; + align-items: center; + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-space-3) 0 var(--uui-size-space-4) 0; + } + + #meta { + margin-left: var(--uui-size-space-4); + line-height: 1.2em; + } + + #name { + font-weight: bold; + } + `, + ]; +} + +export default UmbUserPermissionSettingElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-permission-setting': UmbUserPermissionSettingElement; + } +} diff --git a/src/packages/users/users/index.ts b/src/packages/users/users/index.ts index a58e6a45e7..966b5e5d2d 100644 --- a/src/packages/users/users/index.ts +++ b/src/packages/users/users/index.ts @@ -1,2 +1,3 @@ -export * from './types.js'; +export * from './components/index.js'; export * from './repository/index.js'; +export * from './types.js'; diff --git a/src/packages/users/users/modals/user-picker/user-picker-modal.element.ts b/src/packages/users/users/modals/user-picker/user-picker-modal.element.ts index 9196cf8391..52f6217bec 100644 --- a/src/packages/users/users/modals/user-picker/user-picker-modal.element.ts +++ b/src/packages/users/users/modals/user-picker/user-picker-modal.element.ts @@ -1,46 +1,34 @@ import { UmbUserRepository } from '../../repository/user.repository.js'; -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, state, ifDefined, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import { UmbUserPickerModalData, UmbUserPickerModalResult } from '@umbraco-cms/backoffice/modal'; -import { createExtensionClass } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; import { UmbSelectionManagerBase } from '@umbraco-cms/backoffice/utils'; -import { type UmbUserDetail } from '@umbraco-cms/backoffice/users'; +import { UserItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; @customElement('umb-user-picker-modal') export class UmbUserPickerModalElement extends UmbModalBaseElement { @state() - private _users: Array = []; + private _users: Array = []; #selectionManager = new UmbSelectionManagerBase(); - #userRepository?: UmbUserRepository; + #userRepository = new UmbUserRepository(this); - constructor() { - super(); + connectedCallback(): void { + super.connectedCallback(); - // TODO: this code is reused in multiple places, so it should be extracted to a function - new UmbObserverController( - this, - umbExtensionsRegistry.getByTypeAndAlias('repository', 'Umb.Repository.User'), - async (repositoryManifest) => { - if (!repositoryManifest) return; + // TODO: in theory this config could change during the lifetime of the modal, so we could observe it + this.#selectionManager.setMultiple(this.data?.multiple ?? false); + this.#selectionManager.setSelection(this.data?.selection ?? []); + } - try { - const result = await createExtensionClass(repositoryManifest, [this]); - this.#userRepository = result; - this.#observeUsers(); - } catch (error) { - throw new Error('Could not create repository with alias: Umb.Repository.User'); - } - } - ); + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.#requestUsers(); } - async #observeUsers() { + async #requestUsers() { if (!this.#userRepository) return; - // TODO is this the correct end point? const { data } = await this.#userRepository.requestCollection(); if (data) { @@ -63,14 +51,14 @@ export class UmbUserPickerModalElement extends UmbModalBaseElement html` this.#selectionManager.select(user.id!)} @deselected=${() => this.#selectionManager.deselect(user.id!)} ?selected=${this.#selectionManager.isSelected(user.id!)}> - + - ` + `, )}
diff --git a/src/packages/users/users/repository/user.repository.ts b/src/packages/users/users/repository/user.repository.ts index 02aeb397b7..946820c8ca 100644 --- a/src/packages/users/users/repository/user.repository.ts +++ b/src/packages/users/users/repository/user.repository.ts @@ -80,7 +80,7 @@ export class UmbUserRepository } // COLLECTION - async requestCollection(filter: UmbUserCollectionFilterModel = { skip: 0, take: 100 }) { + async requestCollection(filter: UmbUserCollectionFilterModel = { skip: 0, take: 100000 }) { //TODO: missing observable return this.#collectionSource.filterCollection(filter); } diff --git a/src/packages/users/users/workspace/user-workspace-editor.element.ts b/src/packages/users/users/workspace/user-workspace-editor.element.ts index 843fdc6ca4..ed3b4afb6b 100644 --- a/src/packages/users/users/workspace/user-workspace-editor.element.ts +++ b/src/packages/users/users/workspace/user-workspace-editor.element.ts @@ -1,8 +1,7 @@ import { getDisplayStateFromUserStatus } from '../../utils.js'; -import { UmbUserRepository } from '../repository/user.repository.js'; -import { UmbUserGroupInputElement } from '../../user-groups/components/input-user-group/user-group-input.element.js'; import { type UmbUserDetail } from '../index.js'; import { UmbUserWorkspaceContext } from './user-workspace.context.js'; +import { UmbUserRepository } from '@umbraco-cms/backoffice/users'; import { UUIInputElement, UUIInputEvent, UUISelectElement } from '@umbraco-cms/backoffice/external/uui'; import { css, @@ -15,18 +14,14 @@ import { repeat, } from '@umbraco-cms/backoffice/external/lit'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; - -import { UMB_CHANGE_PASSWORD_MODAL } from '@umbraco-cms/backoffice/modal'; -import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; +import { UMB_CHANGE_PASSWORD_MODAL, type UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; - import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UserStateModel } from '@umbraco-cms/backoffice/backend-api'; -import { createExtensionClass } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; -import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { type UmbUserGroupInputElement } from '@umbraco-cms/backoffice/user-group'; @customElement('umb-user-workspace-editor') export class UmbUserWorkspaceEditorElement extends UmbLitElement { @@ -43,7 +38,7 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement { #modalContext?: UmbModalManagerContext; #workspaceContext?: UmbUserWorkspaceContext; - #userRepository?: UmbUserRepository; + #userRepository = new UmbUserRepository(this); constructor() { super(); @@ -57,22 +52,6 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement { this.#workspaceContext = workspaceContext as UmbUserWorkspaceContext; this.#observeUser(); }); - - // TODO: this code is reused in multiple places, so it should be extracted to a function - new UmbObserverController( - this, - umbExtensionsRegistry.getByTypeAndAlias('repository', 'Umb.Repository.User'), - async (repositoryManifest) => { - if (!repositoryManifest) return; - - try { - const result = await createExtensionClass(repositoryManifest, [this]); - this.#userRepository = result; - } catch (error) { - throw new Error('Could not create repository with alias: Umb.Repository.User'); - } - }, - ); } #observeUser() { @@ -198,9 +177,16 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement { return html`
Profile
- + - +
Assign Access
- +
- Based on the assigned groups and start nodes, the user has access to the following nodes - + Based on the assigned groups and start nodes, the user has access to the following nodes
Content @@ -267,7 +256,7 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement {
Status: - ${this.localize.term('user_'+displayState.key)} + ${this.localize.term('user_' + displayState.key)}
@@ -277,7 +266,11 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement { ` : nothing} - ${this.#renderInfoItem('user_lastLogin', this.localize.date(this._user.lastLoginDate!) || `${this._user.name + ' ' + this.localize.term('user_noLogin') } `)} + ${this.#renderInfoItem( + 'user_lastLogin', + this.localize.date(this._user.lastLoginDate!) || + `${this._user.name + ' ' + this.localize.term('user_noLogin')} `, + )} ${this.#renderInfoItem('user_failedPasswordAttempts', this._user.failedLoginAttempts)} ${this.#renderInfoItem( 'user_lastLockoutDate', @@ -311,28 +304,45 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement { const buttons: TemplateResult[] = []; - if (this._user.state === UserStateModel.DISABLED) { - buttons.push(html` - - `); - } + if (this._user.id !== this._currentUser?.id) { + if (this._user.state === UserStateModel.DISABLED) { + buttons.push(html` + + `); + } - if (this._user.state === UserStateModel.ACTIVE || this._user.state === UserStateModel.INACTIVE) { - buttons.push(html` - - `); + if (this._user.state === UserStateModel.ACTIVE || this._user.state === UserStateModel.INACTIVE) { + buttons.push(html` + + `); + } } if (this._currentUser?.id !== this._user?.id) { const button = html` - + `; buttons.push(button); } buttons.push( - html``, + html``, ); return buttons; diff --git a/src/shared/modal/modal-element.element.ts b/src/shared/modal/modal-element.element.ts index f48b660d33..cd85083498 100644 --- a/src/shared/modal/modal-element.element.ts +++ b/src/shared/modal/modal-element.element.ts @@ -6,7 +6,7 @@ import type { ManifestModal, UmbModalExtensionElement } from '@umbraco-cms/backo export abstract class UmbModalBaseElement< ModalDataType extends object = object, ModalResultType = unknown, - ModalManifestType extends ManifestModal = ManifestModal + ModalManifestType extends ManifestModal = ManifestModal, > extends UmbLitElement implements UmbModalExtensionElement diff --git a/src/shared/repository/repository-items.manager.ts b/src/shared/repository/repository-items.manager.ts index 75f08fb040..991d1aa290 100644 --- a/src/shared/repository/repository-items.manager.ts +++ b/src/shared/repository/repository-items.manager.ts @@ -29,7 +29,7 @@ export class UmbRepositoryItemsManager string | undefined + getUniqueMethod?: (entry: ItemType) => string | undefined, ) { this.host = host; this.#getUnique = getUniqueMethod || ((entry) => entry.id || ''); @@ -46,8 +46,13 @@ export class UmbRepositoryItemsManager