diff --git a/src/components/v-skeleton-loader/v-skeleton-loader.story.ts b/src/components/v-skeleton-loader/v-skeleton-loader.story.ts
index 9b58e5a2fc..dad0f09bc3 100644
--- a/src/components/v-skeleton-loader/v-skeleton-loader.story.ts
+++ b/src/components/v-skeleton-loader/v-skeleton-loader.story.ts
@@ -15,7 +15,7 @@ export const basic = () =>
defineComponent({
props: {
type: {
- default: select('Type', ['input', 'input-tall'], 'input'),
+ default: select('Type', ['input', 'input-tall', 'list-item-icon'], 'input'),
},
},
template: `
diff --git a/src/components/v-skeleton-loader/v-skeleton-loader.vue b/src/components/v-skeleton-loader/v-skeleton-loader.vue
index 3056f29425..ccbe5e2c11 100644
--- a/src/components/v-skeleton-loader/v-skeleton-loader.vue
+++ b/src/components/v-skeleton-loader/v-skeleton-loader.vue
@@ -1,6 +1,11 @@
-
+
@@ -12,6 +17,7 @@ export default defineComponent({
type: {
type: String,
default: 'input',
+ validator: (type: string) => ['input', 'input-tall', 'list-item-icon'].includes(type),
},
},
});
@@ -19,10 +25,18 @@ export default defineComponent({
diff --git a/src/modules/users/index.ts b/src/modules/users/index.ts
index af35eef352..b401a8c5e5 100644
--- a/src/modules/users/index.ts
+++ b/src/modules/users/index.ts
@@ -1,4 +1,5 @@
import { defineModule } from '@/modules/define';
+
import UsersBrowse from './routes/browse/';
import UsersDetail from './routes/detail/';
@@ -8,14 +9,31 @@ export default defineModule(({ i18n }) => ({
icon: 'people',
routes: [
{
- name: 'users-browse',
path: '/',
+ redirect: '/all',
+ },
+ {
+ name: 'users-browse-all',
+ path: '/all',
+ component: UsersBrowse,
+ },
+ {
+ name: 'users-detail-add-new',
+ path: '/+',
+ component: UsersDetail,
+ props: {
+ primaryKey: '+',
+ },
+ },
+ {
+ name: 'users-browse-role',
+ path: '/:role',
component: UsersBrowse,
props: true,
},
{
name: 'users-detail',
- path: '/:primaryKey',
+ path: '/:role/:primaryKey',
component: UsersDetail,
props: true,
},
diff --git a/src/modules/users/routes/browse/browse.vue b/src/modules/users/routes/browse/browse.vue
index 716e4f67fb..c01b13fe20 100644
--- a/src/modules/users/routes/browse/browse.vue
+++ b/src/modules/users/routes/browse/browse.vue
@@ -56,7 +56,9 @@
:selection.sync="selection"
:view-options.sync="viewOptions"
:view-query.sync="viewQuery"
- :detail-route="'/{{project}}/users/{{primaryKey}}'"
+ :detail-route="'/{{project}}/users/{{item.role}}/{{primaryKey}}'"
+ :filters="_filters"
+ @update:filters="filters = $event"
/>
@@ -78,18 +80,50 @@ type Item = {
export default defineComponent({
name: 'users-browse',
components: { UsersNavigation },
- props: {},
- setup() {
+ props: {
+ role: {
+ type: String,
+ default: null,
+ },
+ },
+ setup(props) {
const layout = ref(null);
const projectsStore = useProjectsStore();
const selection = ref- ([]);
- const { viewOptions, viewQuery } = useCollectionPreset(ref('directus_users'));
+ const { viewOptions, viewQuery, filters } = useCollectionPreset(ref('directus_users'));
const { addNewLink, batchLink } = useLinks();
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
const { breadcrumb } = useBreadcrumb();
+ const _filters = computed(() => {
+ if (props.role !== null) {
+ return [
+ {
+ locked: 1,
+ field: 'role',
+ operator: 'eq',
+ value: props.role,
+ },
+ ...filters.value,
+ ];
+ }
+
+ return [
+ // This filter is basically a no-op. Every user has a role. However, by filtering on
+ // this field, we can ensure that the field data is fetched, which is needed to build
+ // out the navigation links
+ {
+ locked: 1,
+ field: 'role',
+ operator: 'nnull',
+ value: 1,
+ },
+ ...filters.value,
+ ];
+ });
+
return {
addNewLink,
batchLink,
@@ -101,6 +135,7 @@ export default defineComponent({
layout,
viewOptions,
viewQuery,
+ _filters,
};
function useBatchDelete() {
diff --git a/src/stores/roles/index.ts b/src/stores/roles/index.ts
new file mode 100644
index 0000000000..3f21615292
--- /dev/null
+++ b/src/stores/roles/index.ts
@@ -0,0 +1,4 @@
+import { useRolesStore } from './roles';
+
+export { useRolesStore };
+export default useRolesStore;
diff --git a/src/stores/roles/roles.ts b/src/stores/roles/roles.ts
new file mode 100644
index 0000000000..9cfa79fcf6
--- /dev/null
+++ b/src/stores/roles/roles.ts
@@ -0,0 +1,25 @@
+import { createStore } from 'pinia';
+import api from '@/api';
+import { useProjectsStore } from '@/stores/projects';
+
+import { Role } from './types';
+
+export const useRolesStore = createStore({
+ id: 'rolesStore',
+ state: () => ({
+ roles: [] as Role[],
+ }),
+ actions: {
+ async hydrate() {
+ const projectsStore = useProjectsStore();
+ const currentProjectKey = projectsStore.state.currentProjectKey;
+
+ const rolesResponse = await api.get(`/${currentProjectKey}/roles`);
+
+ this.state.roles = rolesResponse.data.data;
+ },
+ async dehydrate() {
+ this.reset();
+ },
+ },
+});
diff --git a/src/stores/roles/types.ts b/src/stores/roles/types.ts
new file mode 100644
index 0000000000..0173bc7cd9
--- /dev/null
+++ b/src/stores/roles/types.ts
@@ -0,0 +1,10 @@
+export type Role = {
+ id: number;
+ name: string;
+ description: string;
+ collection_listing: null;
+ module_listing: null;
+ enforce_2fa: null | boolean;
+ external_id: null | string;
+ ip_whitelist: string[];
+};
diff --git a/src/stores/user/types.ts b/src/stores/user/types.ts
index 1dcd1b7dc9..4df7cce301 100644
--- a/src/stores/user/types.ts
+++ b/src/stores/user/types.ts
@@ -1,3 +1,5 @@
+import { Role } from '@/stores/roles/types';
+
export type Avatar = {
data: {
thumbnails: Thumbnail[];
@@ -24,16 +26,7 @@ export type User = {
external_id: string;
'2fa_secret': string;
theme: 'auto' | 'dark' | 'light';
- role: {
- id: number;
- name: string;
- description: string;
- collection_listing: null;
- module_listing: null;
- enforce_2fa: null | boolean;
- external_id: null | string;
- ip_whitelist: string[];
- };
+ role: Role;
password_reset_token: string | null;
timezone: string;
locale: string;
diff --git a/src/utils/filters-to-query/filters-to-query.ts b/src/utils/filters-to-query/filters-to-query.ts
new file mode 100644
index 0000000000..b6375911b1
--- /dev/null
+++ b/src/utils/filters-to-query/filters-to-query.ts
@@ -0,0 +1,12 @@
+import { Filter } from '@/stores/collection-presets/types';
+
+export default function filtersToQuery(filters: readonly Filter[]) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const query: Record = {};
+
+ filters.forEach((filter) => {
+ query[`filter[${filter.field}][${filter.operator}]`] = filter.value;
+ });
+
+ return query;
+}
diff --git a/src/utils/filters-to-query/index.ts b/src/utils/filters-to-query/index.ts
new file mode 100644
index 0000000000..8c8a1b983f
--- /dev/null
+++ b/src/utils/filters-to-query/index.ts
@@ -0,0 +1,4 @@
+import filtersToQuery from './filters-to-query';
+
+export { filtersToQuery };
+export default filtersToQuery;
diff --git a/src/utils/filters-to-query/readme.md b/src/utils/filters-to-query/readme.md
new file mode 100644
index 0000000000..41f63fa46f
--- /dev/null
+++ b/src/utils/filters-to-query/readme.md
@@ -0,0 +1,27 @@
+# Filters to Query
+
+Converts an array of filter objects to an Axios compatible object of query params.
+
+## Usage
+
+```ts
+const filters: Filter[] = [
+ {
+ field: 'title',
+ operator: 'contains',
+ value: 'directus',
+ },
+ {
+ field: 'author',
+ operator: 'eq',
+ value: 1,
+ },
+];
+
+filtersToQuery(filters);
+
+// {
+// 'filter[title][contains]': 'directus',
+// 'filter[author][eq]: 1
+// }
+```
diff --git a/src/views/private/components/module-bar-avatar/module-bar-avatar.test.ts b/src/views/private/components/module-bar-avatar/module-bar-avatar.test.ts
index 65ef6a120d..d51941d22d 100644
--- a/src/views/private/components/module-bar-avatar/module-bar-avatar.test.ts
+++ b/src/views/private/components/module-bar-avatar/module-bar-avatar.test.ts
@@ -127,6 +127,7 @@ describe('Views / Private / Module Bar Avatar', () => {
userStore.state.currentUser = {
id: 1,
avatar: null,
+ role: { id: 15 },
} as any;
const component = shallowMount(ModuleBarAvatar, {
@@ -137,7 +138,7 @@ describe('Views / Private / Module Bar Avatar', () => {
},
});
- expect((component.vm as any).userProfileLink).toBe('/my-project/users/1');
+ expect((component.vm as any).userProfileLink).toBe('/my-project/users/15/1');
expect((component.vm as any).signOutLink).toBe('/my-project/logout');
});
});
diff --git a/src/views/private/components/module-bar-avatar/module-bar-avatar.vue b/src/views/private/components/module-bar-avatar/module-bar-avatar.vue
index 62e90d0aed..b641a2e3c6 100644
--- a/src/views/private/components/module-bar-avatar/module-bar-avatar.vue
+++ b/src/views/private/components/module-bar-avatar/module-bar-avatar.vue
@@ -52,11 +52,10 @@ export default defineComponent({
const userProfileLink = computed(() => {
const project = projectsStore.state.currentProjectKey;
- // This is rendered in the private view, which is only accessible as a logged in user
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const id = userStore.state.currentUser!.id;
+ const id = userStore.state.currentUser?.id;
+ const role = userStore.state.currentUser?.role?.id;
- return `/${project}/users/${id}`;
+ return `/${project}/users/${role}/${id}`;
});
const signOutLink = computed(() => {