diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 2681d484..222e94b7 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -135,24 +135,24 @@ jobs: threshold_alert: 50 threshold_warning: 80 - test-coverage: - name: Publish to Code Climate - needs: - - test-app - - test-frontend - if: needs.test-app.outputs.HAS_CC_SECRETS == 'true' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Restore Coverage Results - uses: actions/download-artifact@v4 - - name: Publish code coverage - uses: paambaati/codeclimate-action@v5 - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - with: - coverageLocations: | - ${{ github.workspace }}/**/lcov.info:lcov - prefix: ${{ github.workplace }} + # test-coverage: + # name: Publish to Code Climate + # needs: + # - test-app + # - test-frontend + # if: needs.test-app.outputs.HAS_CC_SECRETS == 'true' + # runs-on: ubuntu-latest + # timeout-minutes: 10 + # steps: + # - name: Checkout Repository + # uses: actions/checkout@v4 + # - name: Restore Coverage Results + # uses: actions/download-artifact@v4 + # - name: Publish code coverage + # uses: paambaati/codeclimate-action@v5 + # env: + # CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + # with: + # coverageLocations: | + # ${{ github.workspace }}/**/lcov.info:lcov + # prefix: ${{ github.workplace }} diff --git a/app/.eslintrc.js b/app/.eslintrc.js index 9bd438c3..0137b18d 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -31,7 +31,7 @@ module.exports = { 'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }], 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', - quotes: ['error', 'single'], + quotes: ['error', 'single', { avoidEscape: true }], semi: ['error', 'always'] }, overrides: [ diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index acfc189b..caa2364e 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -33,7 +33,7 @@ module.exports = { 'max-len': ['warn', { code: 120, comments: 120, ignoreUrls: true }], 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', - quotes: ['error', 'single'], + quotes: ['error', 'single', { avoidEscape: true }], semi: ['error', 'always'], 'vue/component-tags-order': [ 'error', diff --git a/frontend/src/assets/main.scss b/frontend/src/assets/main.scss index 901bc34f..af54a33f 100644 --- a/frontend/src/assets/main.scss +++ b/frontend/src/assets/main.scss @@ -251,11 +251,26 @@ div:focus-visible { } .p-tag { background-color: $bcbox-primary; - font-size: 1rem; + font-size: .8rem; padding: 0.25rem 0.75rem; font-weight: 400; } +.p-tag-danger { + background-color: #fef1d8; + outline-style: solid; + outline-color: #f8bb47; + outline-width: thin; + + .p-tag-value { + color: #474543; + } + + .p-tag-icon { + color: #474543 + } +} + /* datatable */ .p-datatable, .p-treetable { @@ -299,6 +314,9 @@ div:focus-visible { .p-paginator { justify-content: right; + .p-dropdown-label { + padding-top: 0.2rem; + } } .header-center .p-column-header-content { @@ -312,6 +330,7 @@ div:focus-visible { } .action-buttons { text-align: right; + white-space: nowrap; // width: 150px; } @@ -338,7 +357,8 @@ div:focus-visible { .bcbox-info-dialog { width: 800px; .p-dialog-header { - padding-bottom: 0; + padding: 1rem 1.5rem; + border-bottom: 1px solid #dee2e6; .svg-inline--fa, span[class^="material-icons-"] { @@ -352,12 +372,15 @@ div:focus-visible { @extend h2; } } - + .p-dialog-content{ + padding-top: .5rem; + } .bcbox-info-dialog-subhead { + font-size: 120%; font-weight: normal; - margin-bottom: 1.5rem; - padding-left: 2.6rem; + padding-left: .3rem; @extend .wrap-block; + } } diff --git a/frontend/src/components/bucket/BucketChildConfig.vue b/frontend/src/components/bucket/BucketChildConfig.vue index f3e7bb5c..79945f38 100644 --- a/frontend/src/components/bucket/BucketChildConfig.vue +++ b/frontend/src/components/bucket/BucketChildConfig.vue @@ -92,17 +92,23 @@ const onCancel = () => { Add subfolder +

+ Adding a subfolder below: +
+ {{ props.parentBucket.bucketName }}

- +
{ diff --git a/frontend/src/components/bucket/BucketConfigForm.vue b/frontend/src/components/bucket/BucketConfigForm.vue index 90b97948..a65e6bab 100644 --- a/frontend/src/components/bucket/BucketConfigForm.vue +++ b/frontend/src/components/bucket/BucketConfigForm.vue @@ -1,7 +1,9 @@ diff --git a/frontend/src/components/bucket/BucketList.vue b/frontend/src/components/bucket/BucketList.vue index b40509d6..efe7ccca 100644 --- a/frontend/src/components/bucket/BucketList.vue +++ b/frontend/src/components/bucket/BucketList.vue @@ -58,12 +58,6 @@ onMounted(async () => {

My files

-

- Select a folder to view the files inside it. -

@@ -71,7 +65,7 @@ onMounted(async () => { v-if="usePermissionStore().isUserElevatedRights()" v-tooltip.bottom="'Add a new storage location source'" label="Connect Storage" - class="my-4 p-button-primary" + class="mb-4 mt-1 p-button-primary" data-test="connect-storage" aria-label="Add a new storage location source" @click="showBucketConfig()" @@ -104,14 +98,15 @@ onMounted(async () => {

{{ bucketConfigTitle }}

- + (), {}); const permissionStore = usePermissionStore(); const bucketStore = useBucketStore(); const { getMappedBucketToUserPermissions } = storeToRefs(permissionStore); +const { getUserId } = storeToRefs(useAuthStore()); // State const showSearchUsers: Ref = ref(false); @@ -70,6 +72,8 @@ const updateBucketPermission = (value: boolean, userId: string, permCode: string }; onBeforeMount(async () => { + // TODO: check for any permissions on parent bucket + // transform tale data to prevent overriding parent await permissionStore.fetchBucketPermissions({ bucketId: props.bucketId }); await permissionStore.mapBucketToUserPermissions(props.bucketId); }); @@ -78,6 +82,30 @@ onBeforeMount(async () => { diff --git a/frontend/src/components/bucket/BucketPublicToggle.vue b/frontend/src/components/bucket/BucketPublicToggle.vue new file mode 100644 index 00000000..ffea8038 --- /dev/null +++ b/frontend/src/components/bucket/BucketPublicToggle.vue @@ -0,0 +1,113 @@ + + + diff --git a/frontend/src/components/bucket/BucketSidebar.vue b/frontend/src/components/bucket/BucketSidebar.vue index d125bdf8..74dc8d69 100644 --- a/frontend/src/components/bucket/BucketSidebar.vue +++ b/frontend/src/components/bucket/BucketSidebar.vue @@ -82,7 +82,7 @@ watch(props, () => { aria-describedby="side-panel_desc" >
- info + info

Folder details

+ ios_share + - diff --git a/frontend/src/components/common/SyncButton.vue b/frontend/src/components/common/SyncButton.vue index 75a59885..5b9a9868 100644 --- a/frontend/src/components/common/SyncButton.vue +++ b/frontend/src/components/common/SyncButton.vue @@ -14,10 +14,10 @@ import type { Ref } from 'vue'; type Props = { bucketId?: string; objectId?: string; - recursive: boolean; + recursive?: boolean; labelText?: string; disabled?: boolean; - mode: ButtonMode; + mode?: string; }; const props = withDefaults(defineProps(), { @@ -25,7 +25,8 @@ const props = withDefaults(defineProps(), { objectId: '', recursive: false, labelText: '', - disabled: false + disabled: false, + mode: ButtonMode.ICON }); // State @@ -41,17 +42,19 @@ const { focusedElement } = storeToRefs(useNavStore()); // Dialog const displaySyncDialog = ref(false); +const clicked = ref(false); // Actions const onSubmit = async () => { - try{ + try { if (props.objectId) { await objectStore.syncObject(props.objectId); } else if (props.bucketId) { await bucketStore.syncBucket(props.bucketId, props.recursive); } toast.info('Sync in progress', ''); - } catch(error: any){ + clicked.value = true; + } catch (error: any) { toast.error(error, '', { life: 0 }); } displaySyncDialog.value = false; @@ -166,7 +169,7 @@ const onClick = () => { v-if="props.mode === ButtonMode.ICON" v-tooltip.bottom="{ value: labelText }" class="p-button-lg p-button-text p-button-primary btn-delete-text" - :disabled="props.disabled" + :disabled="clicked || props.disabled" :aria-label="labelText" @click="onClick" > @@ -176,7 +179,7 @@ const onClick = () => { v-else v-tooltip.bottom="{ value: labelText }" class="p-button-outlined btn-delete" - :disabled="props.disabled" + :disabled="clicked || props.disabled" :aria-label="labelText" @click="onClick" > diff --git a/frontend/src/components/object/DeletedObjectTable.vue b/frontend/src/components/object/DeletedObjectTable.vue index 3cc89bbe..116aded2 100644 --- a/frontend/src/components/object/DeletedObjectTable.vue +++ b/frontend/src/components/object/DeletedObjectTable.vue @@ -3,11 +3,7 @@ import { storeToRefs } from 'pinia'; import { onUnmounted, onMounted, ref } from 'vue'; import { Spinner } from '@/components/layout'; -import { - DeleteObjectButton, - ObjectPermission, - RestoreObjectButton -} from '@/components/object'; +import { DeleteObjectButton, ObjectPermission, RestoreObjectButton } from '@/components/object'; import { SyncButton } from '@/components/common'; import { Button, Column, DataTable, Dialog, InputText } from '@/lib/primevue'; import { useAuthStore, useBucketStore, useObjectStore, useNavStore, usePermissionStore } from '@/store'; @@ -80,7 +76,7 @@ async function showPermissions(objectId: string) { focusedElement.value = document.activeElement; } -onMounted( async () => { +onMounted(async () => { loading.value = true; await bucketStore.fetchBuckets({ userId: getUserId.value, objectPerms: true }); @@ -98,21 +94,19 @@ onMounted( async () => { const loadLazyData = (event?: any) => { lazyParams.value = { ...lazyParams.value, first: event?.first || first.value, page: event?.page || 0 }; objectService - .searchObjects( - { - deleteMarker: true, - latest: true, - page: lazyParams.value?.page ? ++lazyParams.value.page : 1, - name: lazyParams.value?.filters?.name.value ? lazyParams.value?.filters?.name.value : undefined, - limit: lazyParams.value.rows, - sort: lazyParams.value.sortField, - order: lazyParams.value.sortOrder === 1 ? 'asc' : 'desc', - } - ) + .searchObjects({ + deleteMarker: true, + latest: true, + page: lazyParams.value?.page ? ++lazyParams.value.page : 1, + name: lazyParams.value?.filters?.name.value ? lazyParams.value?.filters?.name.value : undefined, + limit: lazyParams.value.rows, + sort: lazyParams.value.sortField, + order: lazyParams.value.sortOrder === 1 ? 'asc' : 'desc' + }) .then((r: any) => { // add full object url to table data tableData.value = r.data.map((o: COMSObject) => { - const bucket = getBuckets.value.find((b) => b.bucketId === o.bucketId); + const bucket = getBuckets.value.find((b) => b.bucketId === o.bucketId); return { ...o, location: `${bucket?.endpoint}/${bucket?.bucket}/${o.path}`, @@ -155,7 +149,6 @@ const onFilter = (event?: any) => { onUnmounted(() => { objectStore.setSelectedObjects([]); }); - { aria-label="File details" @click="showInfo(data.id)" > - info - + info + + Set permissions for: +
+ {{ permissionsObjectName }} diff --git a/frontend/src/components/object/ObjectFileDetails.vue b/frontend/src/components/object/ObjectFileDetails.vue index 9ea24fd6..be177f6a 100644 --- a/frontend/src/components/object/ObjectFileDetails.vue +++ b/frontend/src/components/object/ObjectFileDetails.vue @@ -15,7 +15,7 @@ import { ObjectVersion } from '@/components/object'; import { ShareButton } from '@/components/common'; -import { Button, Dialog, Divider } from '@/lib/primevue'; +import { Button, Dialog, Divider, Tag } from '@/lib/primevue'; import { useAuthStore, useBucketStore, @@ -73,7 +73,7 @@ const latestNonDmVersionId = computed(() => getLatestNonDmVersionIdByObjectId.va const isDeleted: Ref = computed(() => getIsDeleted.value(props.objectId)); -async function fetchFileDetails(objectId: string){ +async function fetchFileDetails(objectId: string) { await Promise.all([ versionStore.fetchVersions({ objectId: objectId }), metadataStore.fetchMetadata({ objectId: objectId }), @@ -86,12 +86,11 @@ async function fetchFileDetails(objectId: string){ }); } -async function onObjectDeleted({ hardDelete }: { hardDelete:boolean }) { +async function onObjectDeleted({ hardDelete }: { hardDelete: boolean }) { // if doing hard delete redirect to parent folder if (hardDelete || !bucketVersioningEnabled.value) { - router.push({ path: '/list/objects', query: { bucketId: bucketId.value }}); - } - else { + router.push({ path: '/list/objects', query: { bucketId: bucketId.value } }); + } else { await fetchFileDetails(props.objectId); currentVersionId.value = latestNonDmVersionId.value; } @@ -99,9 +98,9 @@ async function onObjectDeleted({ hardDelete }: { hardDelete:boolean }) { async function onVersionDeleted(changedVersionId: string | undefined, isVersion: boolean, hardDelete: boolean) { // if doing hard delete or no versions left, redirect to parent folder - const otherVersions = allVersions.value.filter(v=>v.id !== changedVersionId); + const otherVersions = allVersions.value.filter((v) => v.id !== changedVersionId); if (hardDelete || (isVersion && otherVersions.length === 0)) { - router.push({ path: '/list/objects', query: { bucketId: bucketId.value }}); + router.push({ path: '/list/objects', query: { bucketId: bucketId.value } }); } // else stay on page else { @@ -111,66 +110,77 @@ async function onVersionDeleted(changedVersionId: string | undefined, isVersion: } async function onVersionCreated() { - await fetchFileDetails(props.objectId) - .then(() => { - currentVersionId.value = latestNonDmVersionId.value; - }); - + await fetchFileDetails(props.objectId).then(() => { + currentVersionId.value = latestNonDmVersionId.value; + }); } onMounted(async () => { const head = await objectStore.headObject(props.objectId); - await permissionStore.fetchBucketPermissions({ userId: getUserId.value, objectPerms: true }); await objectStore.fetchObjects({ objectId: props.objectId, userId: getUserId.value, bucketPerms: true }); object.value = getObject.value(props.objectId); bucketId.value = object.value ? object.value.bucketId : ''; + + await permissionStore.fetchBucketPermissions({ + userId: getUserId.value, + bucketId: bucketId.value, + objectPerms: true + }); if ( - (head?.status !== 204 && !isDeleted.value) && + head?.status !== 204 && + !isDeleted.value && (!object.value || !permissionStore.isObjectActionAllowed(object.value.id, getUserId.value, Permissions.READ, object.value.bucketId)) ) { router.replace({ name: RouteNames.FORBIDDEN }); } // fetch data for child components - await Promise.all([ - bucketStore.fetchBuckets({ bucketId: bucketId.value }), - fetchFileDetails(props.objectId) - ]); + await Promise.all([bucketStore.fetchBuckets({ bucketId: bucketId.value }), fetchFileDetails(props.objectId)]); }); -

+

+ Set permissions for: +
+ {{ object?.name }}

diff --git a/frontend/src/components/object/ObjectList.vue b/frontend/src/components/object/ObjectList.vue index aa77ca57..d9894f7b 100644 --- a/frontend/src/components/object/ObjectList.vue +++ b/frontend/src/components/object/ObjectList.vue @@ -1,6 +1,7 @@