mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42:15 +01:00
add project table visibility filter
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { GlobeAltIcon } from '@heroicons/vue/16/solid';
|
||||||
|
import { DropdownMenuItem } from '@/packages/ui/src';
|
||||||
|
import BaseFilterBadge from './BaseFilterBadge.vue';
|
||||||
|
|
||||||
|
type VisibilityValue = 'public' | 'private' | 'all';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: VisibilityValue;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
remove: [];
|
||||||
|
'update:value': [value: VisibilityValue];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visibilityOptions = [
|
||||||
|
{ id: 'public' as const, name: 'Public' },
|
||||||
|
{ id: 'private' as const, name: 'Private' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const label = computed(() => {
|
||||||
|
return visibilityOptions.find((opt) => opt.id === props.value)?.name ?? 'Visibility';
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateVisibility(visibility: VisibilityValue) {
|
||||||
|
emit('update:value', visibility);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseFilterBadge
|
||||||
|
:icon="GlobeAltIcon"
|
||||||
|
:label="label"
|
||||||
|
filter-name="Visibility"
|
||||||
|
@remove="emit('remove')">
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="option in visibilityOptions"
|
||||||
|
:key="option.id"
|
||||||
|
:class="[value === option.id && 'bg-accent text-accent-foreground']"
|
||||||
|
@click="updateVisibility(option.id)">
|
||||||
|
{{ option.name }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</BaseFilterBadge>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { UserGroupIcon, CheckCircleIcon } from '@heroicons/vue/16/solid';
|
import { UserGroupIcon, CheckCircleIcon, GlobeAltIcon } from '@heroicons/vue/16/solid';
|
||||||
import ListFilterIcon from '@/packages/ui/src/Icons/ListFilterIcon.vue';
|
import ListFilterIcon from '@/packages/ui/src/Icons/ListFilterIcon.vue';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -19,6 +19,7 @@ import { NO_CLIENT_ID } from './constants';
|
|||||||
|
|
||||||
export interface ProjectFilters {
|
export interface ProjectFilters {
|
||||||
status: 'active' | 'archived' | 'all';
|
status: 'active' | 'archived' | 'all';
|
||||||
|
visibility: 'public' | 'private' | 'all';
|
||||||
clientIds: string[];
|
clientIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,11 @@ const statusOptions = [
|
|||||||
{ id: 'archived' as const, name: 'Archived' },
|
{ id: 'archived' as const, name: 'Archived' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const visibilityOptions = [
|
||||||
|
{ id: 'public' as const, name: 'Public' },
|
||||||
|
{ id: 'private' as const, name: 'Private' },
|
||||||
|
];
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
|
|
||||||
function updateStatus(status: 'active' | 'archived' | 'all') {
|
function updateStatus(status: 'active' | 'archived' | 'all') {
|
||||||
@@ -46,6 +52,14 @@ function updateStatus(status: 'active' | 'archived' | 'all') {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateVisibility(visibility: 'public' | 'private' | 'all') {
|
||||||
|
emit('update:filters', {
|
||||||
|
...props.filters,
|
||||||
|
visibility,
|
||||||
|
});
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleClient(clientId: string) {
|
function toggleClient(clientId: string) {
|
||||||
const clientIds = props.filters.clientIds.includes(clientId)
|
const clientIds = props.filters.clientIds.includes(clientId)
|
||||||
? props.filters.clientIds.filter((id) => id !== clientId)
|
? props.filters.clientIds.filter((id) => id !== clientId)
|
||||||
@@ -69,7 +83,11 @@ function toggleNoClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasActiveFilters = computed(() => {
|
const hasActiveFilters = computed(() => {
|
||||||
return props.filters.status !== 'all' || props.filters.clientIds.length > 0;
|
return (
|
||||||
|
props.filters.status !== 'all' ||
|
||||||
|
props.filters.visibility !== 'all' ||
|
||||||
|
props.filters.clientIds.length > 0
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -102,6 +120,25 @@ const hasActiveFilters = computed(() => {
|
|||||||
</DropdownMenuSubContent>
|
</DropdownMenuSubContent>
|
||||||
</DropdownMenuSub>
|
</DropdownMenuSub>
|
||||||
|
|
||||||
|
<!-- Visibility Filter -->
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger class="gap-2">
|
||||||
|
<GlobeAltIcon class="h-4 w-4 text-icon-default" />
|
||||||
|
<span>Visibility</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="option in visibilityOptions"
|
||||||
|
:key="option.id"
|
||||||
|
:class="[
|
||||||
|
filters.visibility === option.id && 'bg-accent text-accent-foreground',
|
||||||
|
]"
|
||||||
|
@click="updateVisibility(option.id)">
|
||||||
|
{{ option.name }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
|
||||||
<!-- Client Filter -->
|
<!-- Client Filter -->
|
||||||
<DropdownMenuSub v-if="clients.length > 0">
|
<DropdownMenuSub v-if="clients.length > 0">
|
||||||
<DropdownMenuSubTrigger class="gap-2">
|
<DropdownMenuSubTrigger class="gap-2">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
|||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import ProjectsFilterDropdown from '@/Components/Common/Project/ProjectsFilterDropdown.vue';
|
import ProjectsFilterDropdown from '@/Components/Common/Project/ProjectsFilterDropdown.vue';
|
||||||
import ProjectStatusFilterBadge from '@/Components/Common/Project/ProjectStatusFilterBadge.vue';
|
import ProjectStatusFilterBadge from '@/Components/Common/Project/ProjectStatusFilterBadge.vue';
|
||||||
|
import ProjectVisibilityFilterBadge from '@/Components/Common/Project/ProjectVisibilityFilterBadge.vue';
|
||||||
import ProjectClientFilterBadge from '@/Components/Common/Project/ProjectClientFilterBadge.vue';
|
import ProjectClientFilterBadge from '@/Components/Common/Project/ProjectClientFilterBadge.vue';
|
||||||
import { NO_CLIENT_ID } from '@/Components/Common/Project/constants';
|
import { NO_CLIENT_ID } from '@/Components/Common/Project/constants';
|
||||||
import type { SortColumn, SortDirection } from '@/Components/Common/Project/ProjectTable.vue';
|
import type { SortColumn, SortDirection } from '@/Components/Common/Project/ProjectTable.vue';
|
||||||
@@ -36,6 +37,7 @@ interface ProjectTableState {
|
|||||||
filters: {
|
filters: {
|
||||||
clientIds: string[];
|
clientIds: string[];
|
||||||
status: 'active' | 'archived' | 'all';
|
status: 'active' | 'archived' | 'all';
|
||||||
|
visibility: 'public' | 'private' | 'all';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ const tableState = useStorage<ProjectTableState>(
|
|||||||
filters: {
|
filters: {
|
||||||
clientIds: [],
|
clientIds: [],
|
||||||
status: 'all',
|
status: 'all',
|
||||||
|
visibility: 'all',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@@ -69,6 +72,14 @@ const filteredProjects = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Visibility filter
|
||||||
|
if (tableState.value.filters.visibility === 'public' && !project.is_public) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tableState.value.filters.visibility === 'private' && project.is_public) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Client filter
|
// Client filter
|
||||||
const hasClientFilter = tableState.value.filters.clientIds.length > 0;
|
const hasClientFilter = tableState.value.filters.clientIds.length > 0;
|
||||||
if (hasClientFilter) {
|
if (hasClientFilter) {
|
||||||
@@ -91,6 +102,10 @@ function removeStatusFilter() {
|
|||||||
tableState.value.filters.status = 'all';
|
tableState.value.filters.status = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeVisibilityFilter() {
|
||||||
|
tableState.value.filters.visibility = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
function removeClientFilter() {
|
function removeClientFilter() {
|
||||||
tableState.value.filters.clientIds = [];
|
tableState.value.filters.clientIds = [];
|
||||||
}
|
}
|
||||||
@@ -152,6 +167,15 @@ const showBillableRate = computed(() => {
|
|||||||
tableState.filters.status = $event as 'active' | 'archived' | 'all'
|
tableState.filters.status = $event as 'active' | 'archived' | 'all'
|
||||||
" />
|
" />
|
||||||
|
|
||||||
|
<ProjectVisibilityFilterBadge
|
||||||
|
v-if="tableState.filters.visibility !== 'all'"
|
||||||
|
data-testid="visibility-filter-badge"
|
||||||
|
:value="tableState.filters.visibility"
|
||||||
|
@remove="removeVisibilityFilter"
|
||||||
|
@update:value="
|
||||||
|
tableState.filters.visibility = $event as 'public' | 'private' | 'all'
|
||||||
|
" />
|
||||||
|
|
||||||
<ProjectClientFilterBadge
|
<ProjectClientFilterBadge
|
||||||
v-if="tableState.filters.clientIds.length > 0"
|
v-if="tableState.filters.clientIds.length > 0"
|
||||||
data-testid="client-filter-badge"
|
data-testid="client-filter-badge"
|
||||||
|
|||||||
Reference in New Issue
Block a user